Reorganize files for new image. Add new paths in home directory. Start on game launchers and installers.

This commit is contained in:
Storm Dragon
2026-04-21 12:21:31 -04:00
parent 1593d15b7e
commit c92d35a023
356 changed files with 2652 additions and 611 deletions
@@ -6,7 +6,7 @@ ConditionPathExists=/sys/class/power_supply
[Service]
Type=simple
ExecStart=/usr/local/bin/battery_monitor.py
ExecStart=/home/stormux/.local/bin/battery_monitor.py
Restart=always
RestartSec=30
User=stormux
@@ -24,4 +24,4 @@ Environment=PULSE_RUNTIME_PATH=/run/user/1000/pulse
# This requires sudoers configuration
[Install]
WantedBy=default.target
WantedBy=default.target
-403
View File
@@ -1,403 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Self-voiced Terminal Menu for Apple IIe Disk Launcher
import os
import sys
import time
import curses
import speechd # Python bindings for Speech Dispatcher
import configparser
class VoicedDiskMenu:
def __init__(self, title="Apple 2e Disk Menu"):
self.title = title
self.menu_items = [] # List to store (name, path) tuples
self.current_index = 0 # Index of current selection
self.stdscr = None
self.curses_initialized = False # Flag to track if curses has been initialized
# Config settings
self.config_dir = os.path.expanduser("~/.config/stormux")
self.config_file = os.path.join(self.config_dir, "apple2e_menu.conf")
self.config = configparser.ConfigParser()
# Default settings
self.speech_rate = 0 # Normal speech rate (0 is default in speechd)
# Load settings
self.load_settings()
# Initialize speech client
self.speech_client = None
self.init_speech()
def init_speech(self):
"""Initialize the speech client"""
try:
self.speech_client = speechd.SSIPClient("apple_menu")
self.speech_client.set_priority(speechd.Priority.IMPORTANT)
self.speech_client.set_punctuation(speechd.PunctuationMode.SOME)
# Apply speech rate from settings
self.speech_client.set_rate(self.speech_rate)
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.config_file):
self.save_settings()
return
try:
self.config.read(self.config_file)
# Load speech settings
if 'Speech' in self.config:
self.speech_rate = self.config.getint('Speech', 'rate', fallback=0)
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.config_dir, exist_ok=True)
# Update config object
if 'Speech' not in self.config:
self.config['Speech'] = {}
self.config['Speech']['rate'] = str(self.speech_rate)
# Write to file
try:
with open(self.config_file, '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.speech_rate = min(100, self.speech_rate + 10) # Max is 100
if self.speech_client:
try:
self.speech_client.set_rate(self.speech_rate)
self.speak(f"Speech rate: {self.speech_rate}")
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.speech_rate = max(-100, self.speech_rate - 10) # Min is -100
if self.speech_client:
try:
self.speech_client.set_rate(self.speech_rate)
self.speak(f"Speech rate: {self.speech_rate}")
except Exception as e:
print(f"Error adjusting speech rate: {e}")
# Save the new setting
self.save_settings()
def speak(self, text, interrupt=True):
"""Speak the given text with option to interrupt existing speech"""
if self.speech_client is None:
return
try:
if interrupt:
self.stop_speech()
self.speech_client.speak(text)
except Exception as e:
# If speech fails, try to reinitialize and try once more
try:
self.init_speech()
if self.speech_client:
self.speech_client.speak(text)
except:
# If reinitializing fails, just give up silently
pass
def stop_speech(self):
"""Stop any ongoing speech"""
if self.speech_client is None:
return
try:
self.speech_client.cancel()
except Exception as e:
# If cancel fails, try to reinitialize
self.init_speech()
def announce_current_item(self, interrupt=True):
"""Announce the currently selected menu item"""
if self.menu_items and 0 <= self.current_index < len(self.menu_items):
name = self.menu_items[self.current_index][0]
self.speak(name, interrupt=interrupt)
def execute_current_item(self):
"""Execute the currently selected menu item"""
if self.menu_items and 0 <= self.current_index < len(self.menu_items):
_, path = self.menu_items[self.current_index]
# Clean up resources before executing the command
self.cleanup(full_cleanup=True)
# Handle special boot options
if path == "TEXTALKER_ONLY":
# Boot with TextTalker only (original Apple 2e option)
os.system('export GAME="Apple 2e" && startx')
elif path.startswith("DISK_ONLY:"):
# Boot with disk only, no TextTalker
os.system(f'export GAME="{path}" && startx')
else:
# Boot with TextTalker + specified disk (default behavior)
os.system(f'export GAME="{path}" && startx')
sys.exit(0) # Exit after launching
def speak_help(self):
"""Speak help information"""
helpText = """
Navigation controls:
Up arrow: Previous disk.
Down arrow: Next disk.
Enter: Launch selected disk.
H key: Hear these instructions again.
Left bracket: Decrease speech rate.
Right bracket: Increase speech rate.
Escape or 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 = "Up/Down: Navigate | Enter: Select | H: Help | [ ] Rate | Q/Esc: Quit"
x = max(0, w // 2 - len(helpText) // 2)
self.stdscr.addstr(3, x, helpText)
# Check if we have items
if not self.menu_items:
message = "No disk files found."
x = max(0, w // 2 - len(message) // 2)
self.stdscr.addstr(5, x, message, curses.A_DIM)
else:
# Show a limited number of items, centered around the current selection
max_display = min(h - 7, len(self.menu_items)) # Max number of items to display
# Calculate starting index for display
half_display = max_display // 2
if self.current_index < half_display:
start_idx = 0
elif self.current_index >= len(self.menu_items) - half_display:
start_idx = max(0, len(self.menu_items) - max_display)
else:
start_idx = self.current_index - half_display
# Draw visible menu items
for i in range(start_idx, min(start_idx + max_display, len(self.menu_items))):
y = (i - start_idx) + 5 # Start items at line 5
# Highlight the selected item
name = self.menu_items[i][0]
if i == self.current_index:
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.speech_rate}"
self.stdscr.addstr(h-2, 2, rateText)
self.stdscr.refresh()
def cleanup(self, full_cleanup=False):
"""Clean up resources before exiting or executing a command
Args:
full_cleanup: If True, also close curses. Used when exiting or running a command.
"""
# Stop any speech
self.stop_speech()
# Close speech client
if self.speech_client:
try:
self.speech_client.close()
except:
pass
self.speech_client = None
# Restore terminal settings if curses was initialized
if full_cleanup and self.curses_initialized:
try:
curses.nocbreak()
self.stdscr.keypad(False)
curses.echo()
curses.endwin()
except:
# If there's an error, just try a simple endwin
try:
curses.endwin()
except:
pass # Last resort, just continue
def load_disks_from_directory(self, directory_path):
"""Load disk files from the specified directory"""
# Add special boot options first
self.menu_items.append(("Boot with TextTalker only", "TEXTALKER_ONLY"))
# Expand the path (in case it contains ~)
directory_path = os.path.expanduser(directory_path)
# Check if directory exists
if not os.path.exists(directory_path) or not os.path.isdir(directory_path):
print(f"Directory {directory_path} does not exist or is not a directory")
return
try:
# Get all .dsk files in the directory
files = [f for f in os.listdir(directory_path) if os.path.isfile(os.path.join(directory_path, f))
and f.lower().endswith(('.dsk', '.do'))]
# Sort files alphabetically
files.sort()
# Create menu items for disk files
for file in files:
# Get full path to the file
file_path = os.path.join(directory_path, file)
# Create display name - remove extension
display_name = os.path.splitext(file)[0]
# Replace underscores with spaces for better readability
display_name = display_name.replace('_', ' ')
# Add both TextTalker + disk and disk-only options
self.menu_items.append((f"{display_name} (with TextTalker)", file_path))
self.menu_items.append((f"{display_name} (disk only)", f"DISK_ONLY:{file_path}"))
except Exception as e:
print(f"Error loading disk directory: {e}")
def run(self):
"""Run the menu system"""
# Check if menu is empty
if not self.menu_items:
message = "No disk files found. Exiting."
print(message)
# Speak the message
self.init_speech()
if self.speech_client:
self.speak(message)
# Wait for speech to finish (rough estimate)
time.sleep(3)
# Clean up and exit properly
self.cleanup(full_cleanup=True)
sys.exit(0)
try:
# Initialize curses
self.stdscr = curses.initscr()
self.curses_initialized = True
curses.noecho()
curses.cbreak()
self.stdscr.keypad(True)
# Initial draw
self.draw_menu()
# Welcome message
self.speak(self.title)
# Wait for initial speech to finish before announcing first item
time.sleep(1)
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
self.current_index = (self.current_index - 1) % len(self.menu_items)
self.draw_menu()
self.announce_current_item()
elif key == curses.KEY_DOWN:
# Move to next item
self.current_index = (self.current_index + 1) % len(self.menu_items)
self.draw_menu()
self.announce_current_item()
elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key
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 == 27 or key == ord('q') or key == ord('Q'): # Esc or Q
break
except Exception as e:
# End curses in case of error
if self.curses_initialized:
try:
curses.endwin()
except:
pass
print(f"An error occurred: {e}")
finally:
# Clean up - safe to call even if curses wasn't initialized
self.cleanup(full_cleanup=True)
# Run the menu
if __name__ == "__main__":
# Create the menu
menu = VoicedDiskMenu()
# Load disk files from the Apple IIe disks directory
menu.load_disks_from_directory("~/.local/games/apple2e/disks/")
# Run the menu
menu.run()
-622
View File
@@ -1,622 +0,0 @@
#!/usr/bin/env python3
import curses
import configparser
import json
import os
import re
import shutil
import subprocess
import sys
import time
from dataclasses import dataclass
try:
import simpleaudio as sa
except Exception:
sa = None
try:
import speechd
except Exception:
speechd = None
FEEDBACK_SOUND = "/usr/share/sounds/stormux/menu_move.wav"
TEST_SOUND = "/usr/share/sounds/stormux/menu_select.wav"
HELP_TEXT = (
"Use left and right arrows to select a control. "
"Use up and down arrows to adjust it. "
"Press space to read the current value. "
"Press h to hear this help again. "
"Press enter to confirm a pending device change or apply the selected device. "
"Press escape or q to exit."
)
def clamp_percent(value, minimum, maximum):
return max(minimum, min(maximum, value))
def format_percent_text(value):
return f"{value} percent"
def cycle_index(current_index, delta, total_count):
if total_count <= 0:
return 0
return (current_index + delta) % total_count
def pending_change_expired(change, now_value=None):
if change is None:
return False
if now_value is None:
now_value = time.time()
return (now_value - change.started_at) >= change.timeout_seconds
def build_default_command(device_kind, device_name):
if device_kind == "sink":
return ["pactl", "set-default-sink", str(device_name)]
if device_kind == "source":
return ["pactl", "set-default-source", str(device_name)]
raise ValueError(f"Unsupported device kind: {device_kind}")
def build_volume_command(device_kind, device_name, volume_percent):
bounded = clamp_percent(volume_percent, 0, 150)
if device_kind == "sink":
return ["pactl", "set-sink-volume", str(device_name), f"{bounded}%"]
if device_kind == "source":
return ["pactl", "set-source-volume", str(device_name), f"{bounded}%"]
raise ValueError(f"Unsupported device kind: {device_kind}")
def is_monitor_source(device_name, description):
lowered_name = (device_name or "").lower()
lowered_description = (description or "").lower()
return lowered_name.endswith(".monitor") or lowered_description.startswith("monitor of ")
def extract_devices(payload, exclude_monitors=False):
devices = []
for item in payload:
device_id = item.get("index")
node_name = item.get("name", "")
properties = item.get("properties") or {}
description = (
properties.get("device.description")
or item.get("description")
or properties.get("node.description")
or node_name
or f"Device {device_id}"
)
if device_id is None:
continue
if exclude_monitors and is_monitor_source(node_name, description):
continue
devices.append(
{
"id": device_id,
"name": description,
"node_name": node_name,
}
)
return devices
def parse_percent_volume(output_text):
match = re.search(r"(\d+)%", output_text)
if not match:
raise RuntimeError(f"Unexpected percent volume output: {output_text}")
return int(match.group(1))
def parse_fraction_volume(output_text):
match = re.search(r"([0-9]*\.?[0-9]+)", output_text)
if not match:
raise RuntimeError(f"Unexpected fractional volume output: {output_text}")
return round(float(match.group(1)) * 100)
def build_external_sound_command(file_path):
player_candidates = [
("pw-play", ["pw-play", file_path]),
("paplay", ["paplay", file_path]),
("aplay", ["aplay", file_path]),
("play", ["play", "-q", file_path]),
]
for executable, command in player_candidates:
if shutil.which(executable) is not None:
return command
return None
def build_feedback_tone_command():
if shutil.which("sox") is None:
return None
return ["sox", "-nqdV0", "synth", ".1", "tri", "840", "fade", ".04", ".1", ".04"]
def find_device_index(devices, *, node_name=None, device_id=None):
for index, device in enumerate(devices):
if device_id is not None and device["id"] == device_id:
return index
if node_name is not None and device["node_name"] == node_name:
return index
return 0
@dataclass
class PendingDeviceChange:
device_kind: str
previous_target: str
candidate_target: str
candidate_name: str
started_at: float = 0.0
timeout_seconds: int = 15
class AudioBackend:
def run_command(self, command):
return subprocess.run(command, capture_output=True, text=True, check=True)
def list_sinks(self):
payload = json.loads(self.run_command(["pactl", "--format=json", "list", "sinks"]).stdout)
return extract_devices(payload)
def list_sources(self):
payload = json.loads(self.run_command(["pactl", "--format=json", "list", "sources"]).stdout)
return extract_devices(payload, exclude_monitors=True)
def get_default_sink_name(self):
return self.run_command(["pactl", "get-default-sink"]).stdout.strip()
def get_default_source_name(self):
return self.run_command(["pactl", "get-default-source"]).stdout.strip()
def set_default_device(self, device_kind, device_name):
self.run_command(build_default_command(device_kind, device_name))
def set_device_volume(self, device_kind, device_name, percent_value):
self.run_command(build_volume_command(device_kind, device_name, percent_value))
def get_device_volume(self, device_kind, device_name):
if device_kind == "sink":
output = self.run_command(["pactl", "get-sink-volume", str(device_name)]).stdout.strip()
elif device_kind == "source":
output = self.run_command(["pactl", "get-source-volume", str(device_name)]).stdout.strip()
else:
raise ValueError(f"Unsupported device kind: {device_kind}")
return parse_percent_volume(output)
class AudioFeedback:
def __init__(self):
self.current_sound = None
self.current_process = None
def stop_sound(self):
if self.current_sound is not None and self.current_sound.is_playing():
self.current_sound.stop()
self.current_sound = None
if self.current_process is not None and self.current_process.poll() is None:
self.current_process.terminate()
self.current_process = None
def play_sound_file(self, file_path):
if sa is not None and os.path.exists(file_path):
try:
self.stop_sound()
self.current_sound = sa.WaveObject.from_wave_file(file_path).play()
return
except Exception:
pass
external_command = build_external_sound_command(file_path)
if external_command is not None:
try:
self.stop_sound()
self.current_process = subprocess.Popen(
external_command,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return
except Exception:
pass
curses.beep()
def play_feedback_beep(self):
tone_command = build_feedback_tone_command()
if tone_command is not None:
try:
self.stop_sound()
self.current_process = subprocess.Popen(
tone_command,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return
except Exception:
pass
self.play_sound_file(FEEDBACK_SOUND)
def play_test_beep(self):
self.play_sound_file(TEST_SOUND)
class AudioManagerApp:
def __init__(self):
self.backend = AudioBackend()
self.feedback = AudioFeedback()
self.speech_client = None
self.config_dir = os.path.expanduser("~/.config/stormux")
self.config_file = os.path.join(self.config_dir, "game_launcher.conf")
self.config = configparser.ConfigParser()
self.speech_rate = 0
self.speech_pitch = 0
self.stdscr = None
self.focus_names = [
"Output Volume",
"Microphone Volume",
"Output Device",
"Microphone Device",
]
self.focus_index = 0
self.output_devices = []
self.input_devices = []
self.output_index = 0
self.input_index = 0
self.default_output_id = None
self.default_input_id = None
self.default_output_name = ""
self.default_input_name = ""
self.output_volume = 50
self.input_volume = 50
self.pending_change = None
self.status_message = ""
self.load_speech_settings()
def load_speech_settings(self):
if not os.path.exists(self.config_file):
return
try:
self.config.read(self.config_file)
if "Speech" in self.config:
self.speech_rate = self.config.getint("Speech", "rate", fallback=0)
self.speech_pitch = self.config.getint("Speech", "pitch", fallback=0)
except Exception:
pass
def init_speech(self):
if speechd is None:
return
self.speech_client = speechd.SSIPClient("audio_manager")
self.speech_client.set_priority(speechd.Priority.IMPORTANT)
self.speech_client.set_punctuation(speechd.PunctuationMode.SOME)
self.speech_client.set_rate(self.speech_rate)
self.speech_client.set_pitch(self.speech_pitch)
def speak(self, text, interrupt=True):
if not self.speech_client:
return
try:
if interrupt:
self.speech_client.cancel()
self.speech_client.speak(text)
except Exception:
pass
def set_status(self, message, speak_message=False):
self.status_message = message
if speak_message:
self.speak(message)
def get_selected_output_device(self):
if not self.output_devices:
return None
return self.output_devices[self.output_index]
def get_selected_input_device(self):
if not self.input_devices:
return None
return self.input_devices[self.input_index]
def refresh_state(self, preserve_selection=False):
previous_output_id = None
previous_input_id = None
if preserve_selection:
selected_output = self.get_selected_output_device()
selected_input = self.get_selected_input_device()
previous_output_id = None if selected_output is None else selected_output["id"]
previous_input_id = None if selected_input is None else selected_input["id"]
self.output_devices = self.backend.list_sinks()
self.input_devices = self.backend.list_sources()
if not self.output_devices:
raise RuntimeError("No output devices found.")
if not self.input_devices:
raise RuntimeError("No microphone devices found.")
default_output_name = self.backend.get_default_sink_name()
default_input_name = self.backend.get_default_source_name()
self.default_output_name = default_output_name
self.default_input_name = default_input_name
self.output_index = find_device_index(
self.output_devices,
device_id=previous_output_id if preserve_selection else None,
node_name=None if preserve_selection else default_output_name,
)
self.input_index = find_device_index(
self.input_devices,
device_id=previous_input_id if preserve_selection else None,
node_name=None if preserve_selection else default_input_name,
)
self.default_output_id = self.output_devices[
find_device_index(self.output_devices, node_name=default_output_name)
]["id"]
self.default_input_id = self.input_devices[
find_device_index(self.input_devices, node_name=default_input_name)
]["id"]
self.output_volume = self.backend.get_device_volume("sink", self.default_output_name)
self.input_volume = self.backend.get_device_volume("source", self.default_input_name)
def apply_volume_delta(self, delta):
try:
if self.focus_index == 0:
new_value = clamp_percent(self.output_volume + delta, 0, 150)
self.backend.set_device_volume("sink", self.default_output_name, new_value)
self.output_volume = new_value
self.feedback.play_feedback_beep()
elif self.focus_index == 1:
new_value = clamp_percent(self.input_volume + delta, 0, 150)
self.backend.set_device_volume("source", self.default_input_name, new_value)
self.input_volume = new_value
self.feedback.play_feedback_beep()
except Exception as error:
self.set_status(f"Volume change failed: {error}", speak_message=True)
def announce_current_focus(self):
if self.focus_index == 0:
self.speak(format_percent_text(self.output_volume))
elif self.focus_index == 1:
self.speak(format_percent_text(self.input_volume))
elif self.focus_index == 2:
device = self.get_selected_output_device()
if device:
self.speak(device["name"])
elif self.focus_index == 3:
device = self.get_selected_input_device()
if device:
self.speak(device["name"])
def move_focus(self, delta):
self.focus_index = cycle_index(self.focus_index, delta, len(self.focus_names))
self.speak(self.focus_names[self.focus_index])
def handle_up(self):
if self.focus_index in (0, 1):
self.apply_volume_delta(5)
return
if self.focus_index == 2 and self.output_devices:
self.output_index = cycle_index(self.output_index, -1, len(self.output_devices))
self.speak(self.output_devices[self.output_index]["name"])
elif self.focus_index == 3 and self.input_devices:
self.input_index = cycle_index(self.input_index, -1, len(self.input_devices))
self.speak(self.input_devices[self.input_index]["name"])
def handle_down(self):
if self.focus_index in (0, 1):
self.apply_volume_delta(-5)
return
if self.focus_index == 2 and self.output_devices:
self.output_index = cycle_index(self.output_index, 1, len(self.output_devices))
self.speak(self.output_devices[self.output_index]["name"])
elif self.focus_index == 3 and self.input_devices:
self.input_index = cycle_index(self.input_index, 1, len(self.input_devices))
self.speak(self.input_devices[self.input_index]["name"])
def start_pending_device_change(self, device_kind, previous_target, candidate):
try:
self.backend.set_default_device(device_kind, candidate["node_name"])
self.pending_change = PendingDeviceChange(
device_kind="output device" if device_kind == "sink" else "microphone device",
previous_target=previous_target,
candidate_target=candidate["node_name"],
candidate_name=candidate["name"],
started_at=time.time(),
)
if device_kind == "sink":
self.default_output_id = candidate["id"]
self.default_output_name = candidate["node_name"]
self.output_volume = self.backend.get_device_volume("sink", self.default_output_name)
self.feedback.play_test_beep()
else:
self.default_input_id = candidate["id"]
self.default_input_name = candidate["node_name"]
self.input_volume = self.backend.get_device_volume("source", self.default_input_name)
self.set_status(
f"{self.pending_change.device_kind.capitalize()} changed to {candidate['name']}. Press enter within 15 seconds to keep it.",
speak_message=True,
)
except Exception as error:
self.set_status(f"Device change failed: {error}", speak_message=True)
def confirm_pending_change(self):
if self.pending_change is None:
return
message = f"{self.pending_change.device_kind.capitalize()} confirmed."
self.pending_change = None
self.refresh_state()
self.set_status(message, speak_message=True)
def rollback_pending_change(self):
if self.pending_change is None:
return
try:
change = self.pending_change
rollback_kind = "sink" if change.device_kind == "output device" else "source"
self.backend.set_default_device(rollback_kind, change.previous_target)
self.pending_change = None
self.refresh_state()
self.set_status(
f"Restored previous {change.device_kind}.",
speak_message=True,
)
except Exception as error:
self.pending_change = None
self.set_status(f"Failed to restore previous device: {error}", speak_message=True)
def activate_selected_device(self):
if self.pending_change is not None:
self.confirm_pending_change()
return
if self.focus_index == 2:
candidate = self.get_selected_output_device()
if candidate is None:
return
if candidate["id"] == self.default_output_id:
self.speak(f"Already using {candidate['name']}")
return
self.start_pending_device_change("sink", self.default_output_name, candidate)
elif self.focus_index == 3:
candidate = self.get_selected_input_device()
if candidate is None:
return
if candidate["id"] == self.default_input_id:
self.speak(f"Already using {candidate['name']}")
return
self.start_pending_device_change("source", self.default_input_name, candidate)
else:
self.announce_current_focus()
def draw(self):
self.stdscr.clear()
self.stdscr.addstr(1, 2, "Stormux Audio Manager", curses.A_BOLD)
output_device = self.get_selected_output_device()
input_device = self.get_selected_input_device()
rows = [
f"Output Volume: {format_percent_text(self.output_volume)}",
f"Microphone Volume: {format_percent_text(self.input_volume)}",
f"Output Device: {'' if output_device is None else output_device['name']}",
f"Microphone Device: {'' if input_device is None else input_device['name']}",
]
for row_index, row_text in enumerate(rows):
attribute = curses.A_REVERSE if row_index == self.focus_index else curses.A_NORMAL
self.stdscr.addstr(4 + row_index, 2, row_text, attribute)
help_text = "Left/Right: Move Up/Down: Change Space: Speak Enter: Apply or Confirm H: Help Q/Esc: Exit"
self.stdscr.addstr(10, 2, help_text)
if self.pending_change is not None:
seconds_left = max(
0,
self.pending_change.timeout_seconds - int(time.time() - self.pending_change.started_at),
)
self.stdscr.addstr(
12,
2,
f"Pending {self.pending_change.device_kind}: confirm within {seconds_left} seconds.",
curses.A_BOLD,
)
if self.status_message:
self.stdscr.addstr(14, 2, self.status_message)
self.stdscr.refresh()
def cleanup(self):
self.feedback.stop_sound()
if self.speech_client is not None:
try:
self.speech_client.close()
except Exception:
pass
if self.stdscr is not None:
try:
curses.nocbreak()
self.stdscr.keypad(False)
curses.echo()
curses.endwin()
except Exception:
pass
def run(self):
try:
self.init_speech()
self.refresh_state()
self.stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
self.stdscr.keypad(True)
self.stdscr.timeout(250)
try:
curses.curs_set(0)
except Exception:
pass
self.speak("Stormux audio manager, press h for help.")
while True:
if pending_change_expired(self.pending_change):
self.rollback_pending_change()
self.draw()
key = self.stdscr.getch()
if key == -1:
continue
if key == curses.KEY_LEFT:
self.move_focus(-1)
elif key == curses.KEY_RIGHT:
self.move_focus(1)
elif key == curses.KEY_UP:
self.handle_up()
elif key == curses.KEY_DOWN:
self.handle_down()
elif key in (10, 13, curses.KEY_ENTER):
self.activate_selected_device()
elif key == ord(" "):
self.announce_current_focus()
elif key in (ord("h"), ord("H")):
self.speak(HELP_TEXT, interrupt=False)
elif key in (ord("q"), ord("Q"), 27):
self.rollback_pending_change()
break
finally:
self.cleanup()
def main():
try:
AudioManagerApp().run()
except Exception as error:
print(f"Audio manager could not start: {error}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
-271
View File
@@ -1,271 +0,0 @@
#!/usr/bin/env python3
"""
Battery Monitor for Stormux Gaming Image
Monitors battery levels and provides warnings at 10% and 5%,
with automatic shutdown at 3% to prevent data loss.
Only activates if a real battery is detected to avoid false alarms.
"""
import os
import sys
import time
import subprocess
import logging
import wave
import numpy as np
from pathlib import Path
try:
import speechd
import simpleaudio as sa
except ImportError as e:
print(f"Required module missing: {e}")
sys.exit(1)
# Set up logging - create log directory if it doesn't exist
try:
log_dir = Path.home() / '.config' / 'stormux'
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / 'battery_monitor.log'
except:
# Fallback to /tmp if home directory not accessible
log_file = '/tmp/battery_monitor.log'
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class BatteryMonitor:
def __init__(self):
# Settings
self.enabled = True
self.warning_10_percent = True
self.warning_5_percent = True
self.shutdown_3_percent = True
self.check_interval = 30
self.speech_enabled = True
# Warning state tracking
self.warned_10 = False
self.warned_5 = False
# Initialize speech client
self.speech_client = None
self.init_speech()
# Ensure config directory exists (handled in logging setup above)
def init_speech(self):
"""Initialize speech-dispatcher client"""
try:
self.speech_client = speechd.SSIPClient("battery_monitor")
self.speech_client.set_priority(speechd.Priority.IMPORTANT)
logger.info("Speech client initialized")
except Exception as e:
logger.error(f"Failed to initialize speech: {e}")
self.speech_enabled = False
def has_battery(self):
"""Check if system has a real battery"""
power_supply_dir = Path('/sys/class/power_supply')
if not power_supply_dir.exists():
return False
for item in power_supply_dir.iterdir():
type_file = item / 'type'
if type_file.exists():
try:
if type_file.read_text().strip() == 'Battery':
logger.info(f"Battery detected: {item.name}")
return True
except Exception:
continue
logger.info("No battery detected")
return False
def get_battery_level(self):
"""Get current battery percentage"""
power_supply_dir = Path('/sys/class/power_supply')
for item in power_supply_dir.iterdir():
type_file = item / 'type'
capacity_file = item / 'capacity'
if (type_file.exists() and capacity_file.exists()):
try:
if type_file.read_text().strip() == 'Battery':
capacity = int(capacity_file.read_text().strip())
return capacity
except Exception:
continue
return None
def is_on_ac_power(self):
"""Check if system is plugged into AC power"""
power_supply_dir = Path('/sys/class/power_supply')
for item in power_supply_dir.iterdir():
type_file = item / 'type'
online_file = item / 'online'
if (type_file.exists() and online_file.exists()):
try:
device_type = type_file.read_text().strip()
if device_type in ['ADP', 'Mains', 'AC']:
online = int(online_file.read_text().strip())
return online == 1
except Exception:
continue
return False
def speak(self, message):
"""Speak message using speech-dispatcher"""
if not self.speech_enabled or not self.speech_client:
logger.warning("Speech not available")
return
try:
self.speech_client.cancel()
self.speech_client.speak(message)
logger.info(f"Speaking: {message}")
except Exception as e:
logger.error(f"Speech error: {e}")
def generate_urgent_sound(self):
"""Generate urgent beep sound in memory"""
try:
# Generate urgent beeping sound
sample_rate = 44100
duration = 0.5
frequency = 800
# Create beep pattern: 3 short beeps
beeps = []
for _ in range(3):
t = np.linspace(0, duration, int(sample_rate * duration))
wave_data = np.sin(2 * np.pi * frequency * t)
# Add fade in/out
fade_samples = int(0.05 * sample_rate)
wave_data[:fade_samples] *= np.linspace(0, 1, fade_samples)
wave_data[-fade_samples:] *= np.linspace(1, 0, fade_samples)
beeps.extend(wave_data)
# Add silence between beeps
beeps.extend([0] * int(0.2 * sample_rate))
# Convert to 16-bit integers
audio_data = np.array(beeps) * 32767
audio_data = audio_data.astype(np.int16)
return audio_data, sample_rate
except Exception as e:
logger.error(f"Sound generation error: {e}")
return None, None
def play_urgent_sound(self):
"""Play urgent warning sound"""
try:
audio_data, sample_rate = self.generate_urgent_sound()
if audio_data is not None:
play_obj = sa.play_buffer(audio_data, 1, 2, sample_rate)
play_obj.wait_done()
logger.info("Urgent sound played")
except Exception as e:
logger.error(f"Sound playback error: {e}")
def handle_low_battery(self, level):
"""Handle low battery warnings and actions"""
# Don't give warnings if we're plugged into AC power
if self.is_on_ac_power():
return
if level <= 3:
if self.shutdown_3_percent:
logger.critical(f"Battery at {level}% - initiating shutdown")
self.speak("Critical battery level. System shutting down now.")
time.sleep(3) # Give speech time to complete
subprocess.run(['sudo', 'systemctl', 'poweroff'], check=True)
return
elif level <= 5 and not self.warned_5:
if self.warning_5_percent:
logger.warning(f"Battery at {level}% - urgent warning")
self.play_urgent_sound()
self.speak("Extremely low battery. Computer will shut down soon.")
self.warned_5 = True
elif level <= 10 and not self.warned_10:
if self.warning_10_percent:
logger.warning(f"Battery at {level}% - first warning")
self.speak("Low battery warning. Please connect power adapter.")
self.warned_10 = True
def reset_warnings_if_charging(self):
"""Reset warning flags if system is charging"""
if self.is_on_ac_power():
if self.warned_10 or self.warned_5:
logger.info("AC power connected - resetting warning flags")
self.warned_10 = False
self.warned_5 = False
def monitor(self):
"""Main monitoring loop"""
if not self.enabled:
logger.info("Battery monitoring disabled")
return
if not self.has_battery():
logger.info("No battery detected - monitoring disabled")
return
logger.info("Starting battery monitoring")
while True:
try:
level = self.get_battery_level()
if level is not None:
logger.debug(f"Battery level: {level}%")
self.reset_warnings_if_charging()
self.handle_low_battery(level)
else:
logger.warning("Could not read battery level")
time.sleep(self.check_interval)
except KeyboardInterrupt:
logger.info("Battery monitoring stopped by user")
break
except Exception as e:
logger.error(f"Monitoring error: {e}")
time.sleep(self.check_interval)
def main():
try:
monitor = BatteryMonitor()
monitor.monitor()
except KeyboardInterrupt:
logger.info("Battery monitor stopped")
except Exception as e:
logger.error(f"Fatal error: {e}")
sys.exit(1)
finally:
# Clean up speech client
try:
if 'monitor' in locals() and monitor.speech_client:
monitor.speech_client.close()
except:
pass
if __name__ == '__main__':
main()
-89
View File
@@ -1,89 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
die() {
echo "$*" >&2
exit 1
}
usage() {
cat <<'EOF' >&2
Usage: convert-to-video.sh /path/to/audio.file
Creates a slideshow video from stormux images and the provided audio.
EOF
exit 1
}
require_commands() {
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || die "Missing dependency: $cmd"
done
}
locate_images() {
local candidate="."
local have_all=true
for i in 1 2 3 4 5; do
[ -f "$candidate/stormux${i}.png" ] || have_all=false
done
if [ "$have_all" = true ]; then
printf '%s\n' "$candidate"
return
fi
candidate="/usr/share/stormux/slideshow"
for i in 1 2 3 4 5; do
[ -f "$candidate/stormux${i}.png" ] || die "Missing image: $candidate/stormux${i}.png"
done
printf '%s\n' "$candidate"
}
detect_container() {
local audio_file="$1"
local codec
codec="$(ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of csv=p=0 "$audio_file" || true)"
case "$codec" in
aac|mp3) echo "mp4" ;;
*) echo "mkv" ;;
esac
}
build_video() {
local audio_file="$1"
local image_dir="$2"
local container="$3"
local output_filename="$4"
local output_dir="$HOME/Videos"
mkdir -p "$output_dir"
ffmpeg -y \
-stream_loop -1 -framerate 1/35 -start_number 1 -i "$image_dir/stormux%d.png" \
-i "$audio_file" \
-map 0:v:0 -map 1:a:0 \
-c:v libx264 -crf 18 -preset veryfast -r 30 -pix_fmt yuv420p \
-c:a copy \
-shortest \
"$output_dir/${output_filename##*/}"
}
[ "$#" -eq 1 ] || usage
audio="$1"
[ -f "$audio" ] || die "Audio file not found: $audio"
require_commands ffmpeg ffprobe
image_dir="$(locate_images)"
container="$(detect_container "$audio")"
base="$(basename "$audio")"
name="${base%.*}"
output="${name}.${container}"
build_video "$audio" "$image_dir" "$container" "$output"
echo "Wrote $HOME/Videos/$output"
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env bash
check_usb_port() {
usb_path=$(udevadm info --query=path --name="$device")
# Check the bus this USB device is on
bus_num=$(echo "$usb_path" | grep -o 'usb[0-9]*' | grep -o '[0-9]*' | head -n1)
# Read the speed from sysfs
speed_file="/sys/bus/usb/devices/usb${bus_num}/speed"
if [[ -f "$speed_file" ]]; then
speed=$(cat "$speed_file")
if [[ $speed -lt 1000 ]]; then
spd-say -Cw "Warning. USB 2.0 detected. Please power off the system and move the USB drive to a USB 3 port for better performance."
fi
fi
}
# set performance (if scaling governor exists)
if [[ -d /sys/devices/system/cpu/cpu0/cpufreq ]]; then
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null || true
fi
# Find the USB bus speed for the boot drive
device=$(findmnt / -o SOURCE -n | sed 's/[0-9]*$//')
if [[ "$device" == /dev/sd* ]]; then
check_usb_port
fi
# File system trim
rootPartition=$(findmnt / -o SOURCE -n)
if [[ $(lsblk -b --discard $rootPartition | awk 'NR==2 { print $NF }') -gt 0 ]]; then
if ! systemctl is-enabled fstrim.timer ; then
sudo systemctl enable fstrim.timer
fi
fi
File diff suppressed because it is too large Load Diff
-556
View File
@@ -1,556 +0,0 @@
#!/usr/bin/env bash
# Stormux Gaming Image - Ultra-Reliable Disk Installer
# Clones USB system to internal disk with proper bootloader installation
set -euo pipefail
# Initialize logging
LOGDIR="/home/stormux/Logs"
LOGFILE="$LOGDIR/install_to_disk.log"
mkdir -p "$LOGDIR"
# Logging function
log() {
echo "$*" | tee -a "$LOGFILE"
}
log_error() {
echo "[ERROR] $*" | tee -a "$LOGFILE" >&2
}
cleanup_mounts() {
# Clean up any leftover mounts
local mount_point="${1:-/mnt/stormux_target}"
if [[ -d "$mount_point" ]]; then
sudo umount "$mount_point/boot" 2>/dev/null || true
sudo umount "$mount_point/proc" 2>/dev/null || true
sudo umount "$mount_point/sys" 2>/dev/null || true
sudo umount "$mount_point/dev" 2>/dev/null || true
sudo umount "$mount_point" 2>/dev/null || true
sudo rmdir "$mount_point" 2>/dev/null || true
fi
}
error_exit() {
log_error "$1"
echo "Installation failed. Log file: $LOGFILE"
cleanup_mounts
restore_speech
echo
read -rp "Press enter to continue..."
exit 1
}
# Speech management
disable_speech() {
echo "command tempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock 2>/dev/null || true
}
restore_speech() {
echo "command toggletempdisablespeech" | socat - UNIX-CLIENT:/tmp/fenrirscreenreader-deamon.sock 2>/dev/null || true
}
# Function to find the source USB device
find_source_device() {
local root_device
root_device=$(findmnt -n -o SOURCE /)
if [[ "$root_device" =~ ^/dev/(sd[a-z]|nvme[0-9]n[0-9]|mmcblk[0-9]) ]]; then
# Extract just the device name (remove partition number)
echo "$root_device" | sed 's/[0-9]*$//' | sed 's/p$//'
else
# Fallback: look for devices with STORMUX label
local labeled_device
labeled_device=$(lsblk -no NAME,LABEL | grep -i stormux | head -1 | awk '{print "/dev/" $1}' | sed 's/[0-9]*$//' | sed 's/p$//')
if [[ -n "$labeled_device" ]]; then
echo "$labeled_device"
else
return 1
fi
fi
}
# Function to detect target disks (excluding source)
detect_target_disks() {
local source_device="$1"
local disks=()
while IFS= read -r disk; do
# Skip partitions, loop devices, CD-ROMs, and source device
if [[ ! "$disk" =~ (sd[a-z][0-9]+|nvme[0-9]+n[0-9]+p[0-9]+|mmcblk[0-9]+p[0-9]+)$ ]] && \
[[ ! "$disk" =~ ^/dev/loop ]] && \
[[ ! "$disk" =~ ^/dev/sr ]] && \
[[ "$disk" != "$source_device" ]]; then
if [[ -b "$disk" ]]; then
disks+=("$disk")
fi
fi
done < <(lsblk -dpno NAME 2>/dev/null)
printf '%s\n' "${disks[@]}"
}
# Function to get disk info
get_disk_info() {
local disk="$1"
local size
local model
size=$(lsblk -dpno SIZE "$disk" 2>/dev/null | tr -d ' ')
model=$(lsblk -dpno MODEL "$disk" 2>/dev/null | tr -d ' ' || echo "Unknown")
echo "$size - $model"
}
# Function to detect partitions by filesystem type
# shellcheck disable=SC2154
detect_partitions() {
local device="$1"
# shellcheck disable=SC2178
local -n result=$2 # nameref to associative array
log "Detecting partition structure on $device..."
# Get all partitions
while IFS= read -r line; do
local part fstype label
part=$(echo "$line" | awk '{print $1}')
# Remove lsblk tree characters that break mount commands
part="${part//[├─└│]/}"
# Add /dev/ prefix if not present
[[ "$part" != /dev/* ]] && part="/dev/$part"
fstype=$(echo "$line" | awk '{print $2}')
label=$(echo "$line" | awk '{print $3}')
# Skip if it's the device itself, not a partition
[[ "$part" == "$device" ]] && continue
case "$fstype" in
"")
# BIOS boot partition (no filesystem)
result[bios]="$part"
log " BIOS boot partition: $part"
;;
"vfat")
# EFI partition
result[efi]="$part"
log " EFI partition: $part (label: ${label:-none})"
;;
"ext4")
# Root partition
result[root]="$part"
log " Root partition: $part (label: ${label:-none})"
;;
*)
log " Unknown partition type: $part ($fstype)"
;;
esac
done < <(lsblk -no NAME,FSTYPE,LABEL "$device" 2>/dev/null | tail -n +2)
# Validate we found all required partitions
if [[ -z "${result[root]:-}" ]]; then
return 1
fi
return 0
}
# Function to regenerate UUIDs and update labels
regenerate_partition_identifiers() {
local device="$1"
local -n partitions=$2
declare -A uuid_map
log "Regenerating partition UUIDs and labels..."
# Process EFI partition
if [[ -n "${partitions[efi]:-}" ]]; then
local efi_part="${partitions[efi]}"
local old_uuid
old_uuid=$(lsblk -no UUID "$efi_part" 2>/dev/null || echo "")
# Change label
if command -v fatlabel >/dev/null 2>&1; then
sudo fatlabel "$efi_part" "BOOT-HDD" 2>/dev/null || log "Warning: Could not rename EFI partition"
fi
# Generate new UUID for FAT
if command -v mlabel >/dev/null 2>&1; then
sudo mlabel -s -i "$efi_part" :: 2>/dev/null || log "Warning: Could not change FAT serial"
fi
local new_uuid
new_uuid=$(lsblk -no UUID "$efi_part" 2>/dev/null || echo "")
if [[ -n "$old_uuid" && -n "$new_uuid" ]]; then
uuid_map["$old_uuid"]="$new_uuid"
log " EFI UUID: $old_uuid -> $new_uuid"
fi
fi
# Process root partition
if [[ -n "${partitions[root]:-}" ]]; then
local root_part="${partitions[root]}"
local old_uuid
old_uuid=$(lsblk -no UUID "$root_part" 2>/dev/null || echo "")
# Change label and UUID
if command -v tune2fs >/dev/null 2>&1; then
sudo tune2fs -L "STORMUX-HDD" "$root_part" 2>/dev/null || log "Warning: Could not rename root partition"
sudo tune2fs -U random "$root_part" 2>/dev/null || log "Warning: Could not change root UUID"
fi
local new_uuid
new_uuid=$(lsblk -no UUID "$root_part" 2>/dev/null || echo "")
if [[ -n "$old_uuid" && -n "$new_uuid" ]]; then
uuid_map["$old_uuid"]="$new_uuid"
log " Root UUID: $old_uuid -> $new_uuid"
fi
fi
# Return UUID mappings
for old in "${!uuid_map[@]}"; do
echo "UUID_MAP:$old:${uuid_map[$old]}"
done
}
# Function to update fstab with new UUIDs
update_fstab() {
local mount_point="$1"
shift
local mappings=("$@")
log "Updating fstab with new UUIDs..."
local fstab="$mount_point/etc/fstab"
if [[ ! -f "$fstab" ]]; then
log_error "fstab not found at $fstab"
return 1
fi
# Backup original
sudo cp "$fstab" "$fstab.backup" || log "Warning: Could not backup fstab"
# Apply UUID mappings
local temp_fstab
temp_fstab=$(mktemp)
sudo cp "$fstab" "$temp_fstab"
for mapping in "${mappings[@]}"; do
if [[ "$mapping" =~ ^UUID_MAP:([^:]+):([^:]+)$ ]]; then
local old_uuid="${BASH_REMATCH[1]}"
local new_uuid="${BASH_REMATCH[2]}"
sed -i "s/UUID=$old_uuid/UUID=$new_uuid/g" "$temp_fstab"
log " Updated fstab: $old_uuid -> $new_uuid"
fi
done
sudo cp "$temp_fstab" "$fstab"
rm -f "$temp_fstab"
log "fstab updated successfully"
return 0
}
# Function to install GRUB in chroot
install_grub() {
local mount_point="$1"
local target_device="$2"
local -n parts=$3
log "Installing GRUB bootloader to $target_device..."
# Ensure EFI partition is mounted inside chroot
if [[ -n "${parts[efi]:-}" ]]; then
sudo mkdir -p "$mount_point/boot"
if ! mountpoint -q "$mount_point/boot"; then
sudo mount "${parts[efi]}" "$mount_point/boot" || error_exit "Failed to mount EFI partition"
log " Mounted EFI partition at /boot"
fi
fi
# Bind mount necessary filesystems for chroot
sudo mount --bind /dev "$mount_point/dev" || error_exit "Failed to bind mount /dev"
sudo mount --bind /sys "$mount_point/sys" || error_exit "Failed to bind mount /sys"
sudo mount --bind /proc "$mount_point/proc" || error_exit "Failed to bind mount /proc"
log " Prepared chroot environment"
# Install GRUB for BIOS (allow to fail gracefully)
log " Installing GRUB for BIOS boot (warnings expected on UEFI systems)..."
if sudo arch-chroot "$mount_point" grub-install --target=i386-pc --recheck "$target_device" 2>&1 | tee -a "$LOGFILE"; then
log " BIOS boot installation succeeded"
else
log " BIOS boot installation completed with warnings (expected on UEFI systems)"
fi
# Install GRUB for UEFI (must succeed)
# Use --removable flag to install to default EFI fallback location
log " Installing GRUB for UEFI boot..."
if ! sudo arch-chroot "$mount_point" grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=STORMUX-HDD --recheck --removable 2>&1 | tee -a "$LOGFILE"; then
error_exit "UEFI GRUB installation failed"
fi
log " UEFI boot installation succeeded"
# Verify /etc/default/grub exists before generating config
if [[ ! -f "$mount_point/etc/default/grub" ]]; then
log_error "/etc/default/grub not found on target system"
error_exit "Cannot generate GRUB config without /etc/default/grub"
fi
# Generate GRUB configuration
log " Generating GRUB configuration..."
if ! sudo arch-chroot "$mount_point" grub-mkconfig -o /boot/grub/grub.cfg 2>&1 | tee -a "$LOGFILE"; then
error_exit "GRUB configuration generation failed"
fi
log " GRUB configuration generated successfully"
# Unmount bind mounts
sudo umount "$mount_point/proc" 2>/dev/null || true
sudo umount "$mount_point/sys" 2>/dev/null || true
sudo umount "$mount_point/dev" 2>/dev/null || true
log "GRUB installation completed successfully"
return 0
}
# Function to validate installation
validate_installation() {
local mount_point="$1"
log "Validating installation..."
# Check fstab exists and has valid entries
if [[ ! -f "$mount_point/etc/fstab" ]]; then
log_error "fstab not found"
return 1
fi
# Check GRUB config exists
if [[ ! -f "$mount_point/boot/grub/grub.cfg" ]]; then
log_error "GRUB configuration not found"
return 1
fi
# Check for baremetal marker
if [[ ! -f "$mount_point/home/stormux/.baremetal" ]]; then
log_error "Baremetal marker not found"
return 1
fi
log "Validation passed"
return 0
}
###################
# Main Installation
###################
clear
log "========================================"
log "Stormux Gaming Image - Disk Installer"
log "========================================"
log "Log file: $LOGFILE"
echo
echo "This will clone the USB system to an internal disk."
echo
# Find source device
log "Detecting source USB device..."
SOURCE_DEVICE=$(find_source_device)
if [[ -z "$SOURCE_DEVICE" ]]; then
error_exit "Could not detect source USB device"
fi
SOURCE_SIZE=$(lsblk -dpno SIZE "$SOURCE_DEVICE" 2>/dev/null | tr -d ' ')
log "Source device: $SOURCE_DEVICE ($SOURCE_SIZE)"
echo "Source device: $SOURCE_DEVICE ($SOURCE_SIZE)"
echo
# Detect target disks
log "Detecting target disks..."
mapfile -t target_disks < <(detect_target_disks "$SOURCE_DEVICE")
if [[ ${#target_disks[@]} -eq 0 ]]; then
error_exit "No suitable target disks found"
fi
# Display target disks
echo "Available target disks:"
log "Available target disks:"
for i in "${!target_disks[@]}"; do
disk="${target_disks[$i]}"
info=$(get_disk_info "$disk")
echo "$((i+1)). $disk - $info"
log " $((i+1)). $disk - $info"
done
# Get disk selection
while true; do
echo
echo "Enter the number of the disk to install to:"
read -r selection
if [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#target_disks[@]} ]]; then
TARGET_DEVICE="${target_disks[$((selection-1))]}"
break
else
echo "Invalid selection. Please enter a number between 1 and ${#target_disks[@]}."
fi
done
log "Selected target device: $TARGET_DEVICE"
# Safety check: ensure target != source
if [[ "$TARGET_DEVICE" == "$SOURCE_DEVICE" ]]; then
error_exit "Target device cannot be the same as source device"
fi
# Check target disk size
TARGET_SIZE_BYTES=$(lsblk -dpno SIZE -b "$TARGET_DEVICE" 2>/dev/null)
SOURCE_SIZE_BYTES=$(lsblk -dpno SIZE -b "$SOURCE_DEVICE" 2>/dev/null)
if [[ "$TARGET_SIZE_BYTES" -lt "$SOURCE_SIZE_BYTES" ]]; then
error_exit "Target disk is smaller than source USB"
fi
# Final confirmation
target_info=$(get_disk_info "$TARGET_DEVICE")
echo
echo "FINAL WARNING:"
echo "Source: $SOURCE_DEVICE ($SOURCE_SIZE)"
echo "Target: $TARGET_DEVICE ($target_info)"
echo "ALL DATA ON THE TARGET DISK WILL BE PERMANENTLY DESTROYED!"
echo
log "Final confirmation - Target: $TARGET_DEVICE"
echo "Type 'yes' to continue or any other key to cancel:"
read -r CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
log "Installation cancelled by user"
echo "Installation cancelled."
exit 0
fi
log "User confirmed installation"
# Disable speech during installation
echo
echo "Fenrir will be silent during installation except for progress beeps."
echo "Press Enter to begin..."
read -r
disable_speech
# Unmount any mounted partitions on target disk
log "Unmounting target disk partitions..."
sudo umount "${TARGET_DEVICE}"* 2>/dev/null || true
# Clone the USB to target disk
log "Starting disk clone operation..."
echo "Cloning USB system to target disk..."
echo "This will take several minutes depending on USB size and disk speed."
echo
if ! sudo dd if="$SOURCE_DEVICE" of="$TARGET_DEVICE" bs=4M oflag=sync status=progress 2>&1 | tee -a "$LOGFILE"; then
error_exit "Failed to clone USB to target disk"
fi
log "Disk clone completed successfully"
# Sync and refresh partition table
log "Syncing data to disk..."
sudo sync
log "Refreshing partition table..."
# Use -s flag for script mode, pipe 'Fix' response for GPT expansion
echo "Fix" | sudo partprobe -s "$TARGET_DEVICE" 2>&1 | tee -a "$LOGFILE" || log "Warning: partprobe reported issues"
sudo udevadm settle --timeout=10 || log "Warning: udevadm settle timeout"
sleep 2
# Detect partition structure
log "Analyzing cloned partition structure..."
declare -A target_partitions
if ! detect_partitions "$TARGET_DEVICE" target_partitions; then
error_exit "Failed to detect partition structure on target disk"
fi
# Validate partition structure
if [[ -z "${target_partitions[root]:-}" ]]; then
error_exit "Could not find root partition on target disk"
fi
log "Partition detection complete:"
log " BIOS: ${target_partitions[bios]:-none}"
log " EFI: ${target_partitions[efi]:-none}"
log " Root: ${target_partitions[root]}"
# Regenerate UUIDs and labels BEFORE mounting
log "Regenerating partition identifiers..."
mapfile -t uuid_mappings < <(regenerate_partition_identifiers "$TARGET_DEVICE" target_partitions)
log "Generated ${#uuid_mappings[@]} UUID mappings"
# Trigger udev to update with new UUIDs
log "Updating system device database..."
sudo udevadm trigger --subsystem-match=block
sudo udevadm settle --timeout=10 || log "Warning: udevadm settle timeout"
sleep 1
# Mount root partition
TEMP_MOUNT="/mnt/stormux_target"
sudo mkdir -p "$TEMP_MOUNT"
log "Mounting root partition ${target_partitions[root]}..."
if ! sudo mount "${target_partitions[root]}" "$TEMP_MOUNT" 2>&1 | tee -a "$LOGFILE"; then
error_exit "Failed to mount root partition"
fi
log "Root partition mounted successfully"
# Update fstab with new UUIDs
if ! update_fstab "$TEMP_MOUNT" "${uuid_mappings[@]}"; then
error_exit "Failed to update fstab"
fi
# Create baremetal marker
log "Creating baremetal system marker..."
sudo touch "$TEMP_MOUNT/home/stormux/.baremetal"
sudo chown 1000:1000 "$TEMP_MOUNT/home/stormux/.baremetal" 2>/dev/null || true
sudo chattr +i "$TEMP_MOUNT/home/stormux/.baremetal" 2>/dev/null || log "Warning: Could not set immutable attribute"
# Remove USB-specific markers
sudo rm -f "$TEMP_MOUNT/home/stormux/.firstboot" 2>/dev/null || true
# Install GRUB
if ! install_grub "$TEMP_MOUNT" "$TARGET_DEVICE" target_partitions; then
error_exit "GRUB installation failed"
fi
# Validate installation
if ! validate_installation "$TEMP_MOUNT"; then
error_exit "Installation validation failed"
fi
# Clean up all mounts
log "Unmounting filesystems..."
cleanup_mounts "$TEMP_MOUNT"
# Restore speech
restore_speech
# Success message
log "========================================="
log "Installation completed successfully!"
log "========================================="
echo
echo "========================================="
echo "Installation completed successfully!"
echo "The USB system has been cloned to $TARGET_DEVICE"
echo "You can now reboot and remove the USB drive."
echo "The system will boot from the internal disk."
echo
echo "Log file: $LOGFILE"
echo "========================================="
echo
echo "Press enter to continue..."
read -r
-185
View File
@@ -1,185 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# IP Address Information Tool for Stormux
# Provides local and remote IP addresses with speech-friendly formatting
import sys
import subprocess
import re
import socket
import urllib.request
import urllib.error
import speechd
def init_speech():
"""Initialize speech client"""
try:
client = speechd.SSIPClient('ip_info')
client.set_priority(speechd.Priority.IMPORTANT)
client.set_punctuation(speechd.PunctuationMode.SOME)
return client
except Exception as e:
print(f"Could not initialize speech: {e}")
return None
def speak(client, text):
"""Speak text using speech client"""
if client:
try:
client.speak(text)
except Exception:
print(text)
else:
print(text)
def format_ip_for_speech(ip_address):
"""Format IP address for clear speech synthesis"""
if not ip_address:
return "No IP address found"
# Split IP into parts and replace dots with "dot"
parts = ip_address.split('.')
if len(parts) != 4:
return ip_address # Return as-is if not a standard IPv4
# Join with " dot " and add spaces between digits for clarity
formatted_parts = []
for part in parts:
# Add spaces between digits for better speech clarity
spaced_digits = ' '.join(part)
formatted_parts.append(spaced_digits)
return ' dot '.join(formatted_parts)
def get_local_ip():
"""Get the local IP address using multiple methods"""
# Method 1: Try to connect to a remote host to determine local IP
try:
# Create a socket and connect to a remote address
# This doesn't actually send data, just determines routing
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(("8.8.8.8", 80))
local_ip = sock.getsockname()[0]
sock.close()
# Validate it's not a loopback address
if not local_ip.startswith('127.'):
return local_ip
except Exception:
pass
# Method 2: Parse ip route command output
try:
result = subprocess.run(['ip', 'route', 'get', '8.8.8.8'],
capture_output=True, text=True, check=True)
# Look for "src" in the output
match = re.search(r'src\s+(\d+\.\d+\.\d+\.\d+)', result.stdout)
if match:
return match.group(1)
except Exception:
pass
# Method 3: Parse ip addr show output for common network interfaces
try:
result = subprocess.run(['ip', 'addr', 'show'],
capture_output=True, text=True, check=True)
# Look for inet addresses that are not loopback
# Common patterns: 192.168.x.x, 10.x.x.x, 172.16-31.x.x
pattern = r'inet\s+(\d+\.\d+\.\d+\.\d+)/\d+'
matches = re.findall(pattern, result.stdout)
for ip in matches:
# Skip loopback
if ip.startswith('127.'):
continue
# Prefer common private network ranges
if (ip.startswith('192.168.') or
ip.startswith('10.') or
re.match(r'^172\.(1[6-9]|2[0-9]|3[01])\.', ip)):
return ip
# If no private IPs found, return the first non-loopback
for ip in matches:
if not ip.startswith('127.'):
return ip
except Exception:
pass
# Method 4: Fallback using hostname
try:
hostname = socket.gethostname()
return socket.gethostbyname(hostname)
except Exception:
pass
return None
def get_remote_ip():
"""Get the remote/public IP address"""
services = [
'https://icanhazip.com',
'https://ipecho.net/plain',
'https://ifconfig.me/ip',
'https://api.ipify.org'
]
for service in services:
try:
with urllib.request.urlopen(service, timeout=10) as response:
ip = response.read().decode('utf-8').strip()
# Validate it looks like an IP address
if re.match(r'^\d+\.\d+\.\d+\.\d+$', ip):
return ip
except Exception:
continue
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in ['local', 'remote']:
print("Usage: ip_info.py [local|remote]")
sys.exit(1)
mode = sys.argv[1]
speech_client = init_speech()
if mode == 'local':
print("Getting local IP address...")
ip = get_local_ip()
if ip:
formatted_ip = format_ip_for_speech(ip)
message = f"Local IP address: {formatted_ip}"
speak(speech_client, message)
print(f"Local IP: {ip}")
else:
message = "Could not determine local IP address"
speak(speech_client, message)
print(message)
elif mode == 'remote':
print("Getting remote IP address...")
ip = get_remote_ip()
if ip:
formatted_ip = format_ip_for_speech(ip)
message = f"Remote IP address: {formatted_ip}"
speak(speech_client, message)
print(f"Remote IP: {ip}")
else:
message = "Could not determine remote IP address. Check internet connection."
speak(speech_client, message)
print(message)
# Clean up speech client
if speech_client:
try:
speech_client.close()
except:
pass
if __name__ == "__main__":
main()
-436
View File
@@ -1,436 +0,0 @@
#!/usr/bin/env python3
# Keymode - Standalone modal keyboard tool for window managers
# Works on both X11 and Wayland
# Written by Storm Dragon https://stormux.org
#
# Copyright (c) 2025 Storm Dragon
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('Gdk', '3.0')
from gi.repository import Gtk, Gdk, GLib
# Try to import GTK Layer Shell for Wayland compositor support
try:
gi.require_version('GtkLayerShell', '0.1')
from gi.repository import GtkLayerShell
HAS_LAYER_SHELL = True
except (ValueError, ImportError):
HAS_LAYER_SHELL = False
import subprocess
import sys
import os
import shutil
from pathlib import Path
import argparse
# TOML loading - try stdlib tomllib (Python 3.11+), fallback to toml package
try:
import tomllib
def load_toml(path):
with open(path, 'rb') as f:
return tomllib.load(f)
except ImportError:
try:
import toml
def load_toml(path):
return toml.load(path)
except ImportError:
print("Error: No TOML parser available.", file=sys.stderr)
print("Install: pip install toml OR upgrade to Python 3.11+", file=sys.stderr)
sys.exit(1)
def get_config_path():
"""Get the path to the config file using XDG_CONFIG_HOME"""
config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
return Path(config_home) / 'stormux' / 'keymode.toml'
def play_sound_async(command):
"""Play a sound asynchronously without blocking (pattern from sound.py:24-36)"""
if not command or not shutil.which('play'):
return
try:
subprocess.Popen(
command,
shell=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
except Exception:
# Silently ignore sound playback errors
pass
def play_sound_reversed(command):
"""Play sound in reverse by appending 'reverse' to sox command (pattern from sound.py:79)"""
if not command:
return
reversed_command = command + " reverse"
play_sound_async(reversed_command)
def execute_command(command):
"""Execute configured command asynchronously"""
if not command:
return
try:
# Expand ~ and environment variables
expanded_command = os.path.expanduser(command)
expanded_command = os.path.expandvars(expanded_command)
# Execute in background
subprocess.Popen(
expanded_command,
shell=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
except Exception as e:
print(f"Error executing command: {e}", file=sys.stderr)
class KeyMode_Window(Gtk.Window):
"""Main window for modal keyboard capture"""
def __init__(self, modeName, config):
super().__init__(title=f"keymode-{modeName}")
self.modeName = modeName
self.config = config
self.modeConfig = config['mode'][modeName]
# Initialize GTK Layer Shell if available (for Wayland)
# This allows the window to receive focus even when other windows are fullscreen
if HAS_LAYER_SHELL and GtkLayerShell.is_supported():
GtkLayerShell.init_for_window(self)
# Use overlay layer to ensure focus even with fullscreen windows
GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY)
# Request keyboard interactivity
GtkLayerShell.set_keyboard_mode(self, GtkLayerShell.KeyboardMode.EXCLUSIVE)
# Center on screen
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.TOP, False)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.BOTTOM, False)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.LEFT, False)
GtkLayerShell.set_anchor(self, GtkLayerShell.Edge.RIGHT, False)
# Window properties for X11 and Wayland compatibility
self.set_default_size(200, 80)
self.set_decorated(False)
self.set_skip_taskbar_hint(True)
self.set_skip_pager_hint(True)
self.set_keep_above(True)
# Center window on screen (for X11 and non-layer-shell Wayland)
self.set_position(Gtk.WindowPosition.CENTER)
# Optional: try to set semi-transparent background
try:
screen = self.get_screen()
visual = screen.get_rgba_visual()
if visual:
self.set_visual(visual)
except Exception:
pass
# Create label showing mode name
description = self.modeConfig.get('description', modeName)
label = Gtk.Label(label=description)
label.set_margin_top(20)
label.set_margin_bottom(20)
label.set_margin_start(20)
label.set_margin_end(20)
# Set accessible name for screen readers
try:
accessible = label.get_accessible()
if accessible:
accessible.set_name(f"Mode: {description}")
except Exception:
pass
self.add(label)
# Connect signals
self.connect("realize", self.on_realize)
self.connect("key-press-event", self.on_key_press)
# Timeout handling
timeout = config.get('settings', {}).get('timeout_seconds', 0)
if timeout > 0:
GLib.timeout_add_seconds(timeout, self.on_timeout)
def on_realize(self, widget):
"""Called when window is realized - grab focus and play sound"""
# Grab keyboard focus
self.get_window().focus(Gdk.CURRENT_TIME)
# Play entry sound
sound = self.modeConfig.get('sound')
if sound:
play_sound_async(sound)
def on_timeout(self):
"""Called when timeout expires"""
# Play reversed sound
sound = self.modeConfig.get('sound')
if sound:
play_sound_reversed(sound)
# Exit
Gtk.main_quit()
return False
def keyval_to_config_string(self, event):
"""Convert Gdk.Event to config string like 'Control+c' or 'F1'"""
modifiers = []
# Check for modifier keys
if event.state & Gdk.ModifierType.CONTROL_MASK:
modifiers.append("Control")
if event.state & Gdk.ModifierType.MOD1_MASK: # Alt
modifiers.append("Alt")
if event.state & Gdk.ModifierType.SHIFT_MASK:
modifiers.append("Shift")
if event.state & Gdk.ModifierType.MOD4_MASK: # Super
modifiers.append("Super")
# Get key name
keyName = Gdk.keyval_name(event.keyval)
# Build config string
if modifiers:
return "+".join(modifiers + [keyName])
return keyName
def on_key_press(self, widget, event):
"""Handle keyboard input"""
# Convert keyval to config string
keyString = self.keyval_to_config_string(event)
# Check if Escape (exit without action)
if event.keyval == Gdk.KEY_Escape:
sound = self.modeConfig.get('sound')
if sound:
play_sound_reversed(sound)
Gtk.main_quit()
return True
# Look up in config
keys = self.modeConfig.get('keys', {})
if keyString in keys:
command = keys[keyString]
# Play reversed sound
sound = self.modeConfig.get('sound')
if sound:
play_sound_reversed(sound)
# Execute command
execute_command(command)
# Exit
Gtk.main_quit()
return True
# Unknown key - ignore
return True
def generate_example_config(configPath):
"""Generate example config file"""
configPath.parent.mkdir(parents=True, exist_ok=True)
example = """# Keymode Configuration
# Configuration for modal keyboard interaction tool
[settings]
# Timeout in seconds (0 = no timeout, waits indefinitely)
timeout_seconds = 0
# Example mode: ratpoison-style application launcher
[mode.ratpoison]
description = "Ratpoison Mode"
# Sound played when entering mode (sox command)
# Will be played in reverse when exiting
sound = "play -qV0 \\"|sox -np synth .07 sq 400\\" \\"|sox -np synth .5 sq 800\\" fade h 0 .5 .5 norm -20"
[mode.ratpoison.keys]
# Format: key = "command to execute"
# Special keys use quotes: "F1", "F2", etc.
# Modifiers: "Control+c", "Alt+f", "Shift+x", "Super+r"
# Escape always exits without action (built-in)
# Unbound keys are ignored - the mode waits for a valid key
c = "lxterminal"
w = "brave"
# Window Manager Integration Examples:
#
# i3/Sway (~/.config/i3/config or ~/.config/sway/config):
# bindsym Escape exec ~/.config/i3/scripts/keymode.py --mode ratpoison
#
# Openbox (~/.config/openbox/rc.xml):
# <keybind key="Escape">
# <action name="Execute">
# <command>~/.config/i3/scripts/keymode.py --mode ratpoison</command>
# </action>
# </keybind>
#
# Fluxbox (~/.fluxbox/keys):
# Escape :Exec ~/.config/i3/scripts/keymode.py --mode ratpoison
"""
with open(configPath, 'w') as f:
f.write(example)
print(f"Example config generated at: {configPath}")
def validate_config(config, modeName):
"""Validate configuration and provide helpful error messages"""
if 'mode' not in config:
print("Error: No modes defined in config.toml", file=sys.stderr)
sys.exit(1)
if modeName not in config['mode']:
available = ', '.join(config['mode'].keys())
print(f"Error: Mode '{modeName}' not found.", file=sys.stderr)
print(f"Available modes: {available}", file=sys.stderr)
sys.exit(1)
modeConfig = config['mode'][modeName]
if 'keys' not in modeConfig or not modeConfig['keys']:
print(f"Error: Mode '{modeName}' has no keybindings defined", file=sys.stderr)
sys.exit(1)
return True
def list_modes(config):
"""List available modes from config"""
if 'mode' not in config or not config['mode']:
print("No modes defined in config")
return
print("Available modes:")
for modeName, modeConfig in config['mode'].items():
description = modeConfig.get('description', 'No description')
numKeys = len(modeConfig.get('keys', {}))
print(f" {modeName}: {description} ({numKeys} keybindings)")
def parse_args():
"""Parse command-line arguments"""
parser = argparse.ArgumentParser(
description="Modal keyboard input handler for window manager by Storm Dragon https://stormux.org",
epilog="Example: keymode --mode ratpoison"
)
parser.add_argument(
'--mode', '-m',
help='Mode name from config.toml (e.g., ratpoison)'
)
parser.add_argument(
'--config', '-c',
help=f'Path to config file (default: {get_config_path()})'
)
parser.add_argument(
'--generate-config',
action='store_true',
help='Generate example config and exit'
)
parser.add_argument(
'--list-modes',
action='store_true',
help='List available modes from config'
)
parser.add_argument(
'--version', '-v',
action='version',
version='keymode 2025.12.13'
)
return parser.parse_args()
def main():
"""Main entry point"""
args = parse_args()
# Get config path
configPath = Path(args.config) if args.config else get_config_path()
# Handle --generate-config
if args.generate_config:
generate_example_config(configPath)
return 0
# Check if config exists
if not configPath.exists():
print(f"Error: Config file not found: {configPath}", file=sys.stderr)
print(f"Run: {sys.argv[0]} --generate-config", file=sys.stderr)
return 1
# Load config
try:
config = load_toml(str(configPath))
except Exception as e:
print(f"Error loading config: {e}", file=sys.stderr)
return 1
# Handle --list-modes
if args.list_modes:
list_modes(config)
return 0
# Require --mode for normal operation
if not args.mode:
print("Error: --mode is required", file=sys.stderr)
print(f"Run: {sys.argv[0]} --list-modes to see available modes", file=sys.stderr)
return 1
# Validate config
validate_config(config, args.mode)
# Check if sox is available (warn but continue)
if not shutil.which('play'):
print("Warning: 'play' command not found. Audio feedback disabled.", file=sys.stderr)
print("Install sox for sound effects: sudo apt install sox", file=sys.stderr)
# Create and show window
window = KeyMode_Window(args.mode, config)
window.show_all()
# Run GTK main loop
Gtk.main()
return 0
if __name__ == '__main__':
sys.exit(main())
-99
View File
@@ -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
-621
View File
@@ -1,621 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import time
import threading
import signal
import curses
import subprocess
import speechd
import configparser
import pathlib
import re
import random
class VoicedMusicPlayer:
def __init__(self, title="Stormux Music Player"):
self.title = title
self.menuSections = {}
self.sectionNames = []
self.currentSection = 0
self.currentItemIndices = {}
self.stdscr = None
self.cursesInitialized = False
self.hasItems = False
self.navigationStack = []
self.currentView = "main"
self.currentAlbumPath = None
self.currentAlbumName = None
self.configDir = os.path.expanduser("~/.config/stormux")
self.configFile = os.path.join(self.configDir, "music_player.conf")
self.config = configparser.ConfigParser()
self.speechRate = 0
self.randomize = False
self.musicExtensions = ['.mp3', '.flac', '.ogg', '.wav', '.opus']
self.musicDir = os.path.expanduser("~/Music")
self.load_settings()
self.speechClient = None
self.init_speech()
def init_speech(self):
try:
self.speechClient = speechd.SSIPClient("music_player")
self.speechClient.set_priority(speechd.Priority.IMPORTANT)
self.speechClient.set_punctuation(speechd.PunctuationMode.SOME)
self.speechClient.set_rate(self.speechRate)
except Exception as e:
print(f"Could not initialize speech: {e}")
def load_settings(self):
if not os.path.exists(self.configFile):
self.save_settings()
return
try:
self.config.read(self.configFile)
if 'Speech' in self.config:
self.speechRate = self.config.getint('Speech', 'rate', fallback=0)
if 'Player' in self.config:
self.randomize = self.config.getboolean('Player', 'randomize', fallback=False)
except Exception as e:
print(f"Error loading settings: {e}")
def save_settings(self):
os.makedirs(self.configDir, exist_ok=True)
if 'Speech' not in self.config:
self.config['Speech'] = {}
if 'Player' not in self.config:
self.config['Player'] = {}
self.config['Speech']['rate'] = str(self.speechRate)
self.config['Player']['randomize'] = str(self.randomize)
try:
with open(self.configFile, 'w') as f:
self.config.write(f)
except Exception as e:
print(f"Error saving settings: {e}")
def toggle_randomize(self):
self.randomize = not self.randomize
self.save_settings()
if self.randomize:
self.speak("Random playback on")
else:
self.speak("Sequential playback")
def increase_speech_rate(self):
self.speechRate = min(100, self.speechRate + 10)
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}")
self.save_settings()
def decrease_speech_rate(self):
self.speechRate = max(-100, self.speechRate - 10)
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}")
self.save_settings()
def add_section(self, sectionName):
if sectionName not in self.menuSections:
self.menuSections[sectionName] = []
self.sectionNames.append(sectionName)
self.currentItemIndices[sectionName] = 0
def add_item(self, sectionName, name, command, isDirectory=False, directoryPath=None):
if sectionName not in self.menuSections:
self.add_section(sectionName)
itemData = (name, command, isDirectory, directoryPath)
self.menuSections[sectionName].append(itemData)
self.hasItems = True
def speak(self, text, interrupt=True):
if self.speechClient is None:
return
try:
if interrupt:
self.stop_speech()
self.speechClient.speak(text)
except Exception as e:
try:
self.init_speech()
if self.speechClient:
self.speechClient.speak(text)
except:
pass
def stop_speech(self):
if self.speechClient is None:
return
try:
self.speechClient.cancel()
except Exception as e:
self.init_speech()
def get_current_items(self):
if not self.sectionNames:
return []
currentSectionName = self.sectionNames[self.currentSection]
return self.menuSections[currentSectionName]
def get_current_item_index(self):
if not self.sectionNames:
return 0
currentSectionName = self.sectionNames[self.currentSection]
return self.currentItemIndices[currentSectionName]
def set_current_item_index(self, index):
if not self.sectionNames:
return
currentSectionName = self.sectionNames[self.currentSection]
self.currentItemIndices[currentSectionName] = index
def announce_current_section(self, interrupt=True):
if 0 <= self.currentSection < len(self.sectionNames):
sectionName = self.sectionNames[self.currentSection]
self.speak(sectionName, interrupt=interrupt)
def announce_current_item(self, interrupt=True):
if len(self.sectionNames) > 0:
items = self.get_current_items()
currentIndex = self.get_current_item_index()
if items and 0 <= currentIndex < len(items):
name = items[currentIndex][0]
isDirectory = items[currentIndex][2]
if isDirectory:
self.speak(f"{name}, folder", interrupt=interrupt)
else:
self.speak(name, interrupt=interrupt)
else:
self.speak("No items", interrupt=interrupt)
def get_music_files_in_dir(self, directoryPath):
musicFiles = []
try:
files = sorted([f for f in os.listdir(directoryPath) if os.path.isfile(os.path.join(directoryPath, f))])
for file in files:
filePath = os.path.join(directoryPath, file)
fileExt = os.path.splitext(file)[1].lower()
if fileExt in self.musicExtensions:
musicFiles.append(filePath)
except Exception as e:
print(f"Error getting music files: {e}")
return musicFiles
def execute_current_item(self):
if len(self.sectionNames) > 0:
items = self.get_current_items()
index = self.get_current_item_index()
if 0 <= index < len(items):
name, command, isDirectory, directoryPath = items[index]
if isDirectory and directoryPath:
self.open_album(directoryPath, name)
return
# Build the base mpv command
shuffle_flag = "--shuffle" if self.randomize else ""
base_cmd = f"mpv --no-video --really-quiet {shuffle_flag}".strip()
if name == "Play All Music":
command = f'{base_cmd} "{self.musicDir}"'
elif name == "Play All Root Music":
# For root only, we do need the glob to avoid subdirectories
command = f'{base_cmd} "{self.musicDir}"/*'
elif name.startswith("Play All ") and not isDirectory:
# This handles both artist "Play All [Artist Name]" and album "Play All [Album Name]"
if self.currentView == "album":
# We're in album view, use the current album path
album_path = self.currentAlbumPath
if album_path:
command = f'{base_cmd} "{album_path}"'
else:
# We're in main view, this is a "Play All [Artist Name]" command
# The directoryPath should contain the actual artist directory path
if directoryPath and os.path.exists(directoryPath):
command = f'{base_cmd} "{directoryPath}"'
elif name.startswith("Play All ") and self.currentView == "album" and not isDirectory:
# Album playback
album_path = self.currentAlbumPath
if album_path:
command = f'{base_cmd} "{album_path}"'
if command:
self.cleanup(fullCleanup=True)
os.system(command)
os.execv(sys.executable, ['python3'] + sys.argv)
def open_album(self, albumPath, albumName):
oldSections = self.menuSections.copy()
oldSectionNames = self.sectionNames.copy()
oldCurrentSection = self.currentSection
oldCurrentIndices = self.currentItemIndices.copy()
self.navigationStack.append({
'sections': oldSections,
'section_names': oldSectionNames,
'current_section': oldCurrentSection,
'current_indices': oldCurrentIndices,
'view': self.currentView
})
self.menuSections = {}
self.sectionNames = []
self.currentItemIndices = {}
self.currentSection = 0
self.currentView = "album"
self.currentAlbumPath = albumPath
self.currentAlbumName = albumName
albumSection = f"{albumName}"
self.add_section(albumSection)
musicFiles = self.get_music_files_in_dir(albumPath)
if musicFiles:
self.add_item(albumSection, f"Play All {albumName}", "")
self.add_item(albumSection, "Back to Artist", "", isDirectory=False)
for filePath in musicFiles:
fileName = os.path.basename(filePath)
displayName = os.path.splitext(fileName)[0].replace('_', ' ')
command = f'mpv --no-video --really-quiet "{filePath}"'
self.add_item(albumSection, displayName, command)
else:
self.add_item(albumSection, "Back to Artist", "", isDirectory=False)
self.draw_menu()
self.announce_current_section()
time.sleep(0.5)
self.announce_current_item(interrupt=False)
def go_back(self):
if self.navigationStack:
prevState = self.navigationStack.pop()
self.menuSections = prevState['sections']
self.sectionNames = prevState['section_names']
self.currentSection = prevState['current_section']
self.currentItemIndices = prevState['current_indices']
self.currentView = prevState['view']
if self.currentView != "album":
self.currentAlbumPath = None
self.currentAlbumName = None
self.draw_menu()
self.announce_current_section()
time.sleep(0.5)
self.announce_current_item(interrupt=False)
return True
return False
def speak_help(self):
helpText = """
Navigation controls:
Up arrow: Previous menu item.
Down arrow: Next menu item.
Left arrow: Previous artist.
Right arrow: Next artist.
Enter: Play selected item or enter album.
Backspace: Go back to previous view.
R key: Toggle random playback.
H key: Hear these instructions again.
Left bracket: Decrease speech rate.
Right bracket: Increase speech rate.
Escape or Q: Exit the menu.
Any key will interrupt speech.
"""
self.speak(helpText)
def draw_menu(self):
self.stdscr.clear()
h, w = self.stdscr.getmaxyx()
title = f" {self.title} "
x = max(0, w // 2 - len(title) // 2)
self.stdscr.addstr(1, x, title, curses.A_BOLD)
helpText = "Navigate | Enter: Select | R: Random | H: Help | [ ] Rate | Q/Esc: Quit"
x = max(0, w // 2 - len(helpText) // 2)
self.stdscr.addstr(3, x, helpText)
randomText = "Mode: Random" if self.randomize else "Mode: Sequential"
self.stdscr.addstr(3, w - len(randomText) - 2, randomText)
if self.currentView == "album" and self.currentAlbumName:
contextText = f"Album: {self.currentAlbumName}"
x = max(0, w // 2 - len(contextText) // 2 - 10)
self.stdscr.addstr(5, x, contextText, curses.A_DIM)
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)
items = self.get_current_items()
currentItemIndex = self.get_current_item_index()
if not items:
emptyMsg = "No items in this section"
x = max(0, w // 2 - len(emptyMsg) // 2)
self.stdscr.addstr(7, x, emptyMsg, curses.A_DIM)
else:
for i, (name, _, isDirectory, _) in enumerate(items):
y = i + 7
if y < h - 1:
if i == currentItemIndex:
prefix = " > "
attr = curses.A_REVERSE
else:
prefix = " "
attr = curses.A_NORMAL
if isDirectory:
text = f"{prefix}{name} [Album]"
else:
text = f"{prefix}{name}"
x = max(0, w // 2 - len(text) // 2)
self.stdscr.addstr(y, x, text, attr)
rateText = f"Speech Rate: {self.speechRate}"
self.stdscr.addstr(h-2, 2, rateText)
self.stdscr.refresh()
def cleanup(self, fullCleanup=False):
self.stop_speech()
if self.speechClient:
try:
self.speechClient.close()
except:
pass
self.speechClient = None
if fullCleanup and self.cursesInitialized:
try:
curses.nocbreak()
self.stdscr.keypad(False)
curses.echo()
curses.endwin()
except:
try:
curses.endwin()
except:
pass
def load_music_from_directory(self, directoryPath):
directoryPath = os.path.expanduser(directoryPath)
self.musicDir = directoryPath
if not os.path.exists(directoryPath) or not os.path.isdir(directoryPath):
print(f"Directory {directoryPath} does not exist or is not a directory")
return
rootMusicFiles = self.get_music_files_in_dir(directoryPath)
musicDirs = [d for d in os.listdir(directoryPath) if os.path.isdir(os.path.join(directoryPath, d))]
for artistDir in sorted(musicDirs):
artistPath = os.path.join(directoryPath, artistDir)
artistName = artistDir.replace('_', ' ')
self.add_section(artistName)
artistAllMusicFiles = []
artistMusicFiles = self.get_music_files_in_dir(artistPath)
artistAllMusicFiles.extend(artistMusicFiles)
albumDirs = [d for d in os.listdir(artistPath) if os.path.isdir(os.path.join(artistPath, d))]
for albumDir in albumDirs:
albumPath = os.path.join(artistPath, albumDir)
albumMusicFiles = self.get_music_files_in_dir(albumPath)
artistAllMusicFiles.extend(albumMusicFiles)
if artistAllMusicFiles:
self.add_item(artistName, f"Play All {artistName}", "", isDirectory=False, directoryPath=artistPath)
for albumDir in sorted(albumDirs):
albumPath = os.path.join(artistPath, albumDir)
albumName = albumDir.replace('_', ' ')
albumMusicFiles = self.get_music_files_in_dir(albumPath)
if albumMusicFiles:
self.add_item(artistName, albumName, "", isDirectory=True, directoryPath=albumPath)
for filePath in sorted(artistMusicFiles):
fileName = os.path.basename(filePath)
displayName = os.path.splitext(fileName)[0].replace('_', ' ')
command = f'mpv --no-video --really-quiet "{filePath}"'
self.add_item(artistName, displayName, command)
hasAnyMusic = bool(rootMusicFiles) or bool(musicDirs)
if hasAnyMusic:
self.add_section("All Music")
self.add_item("All Music", "Play All Music", "")
if rootMusicFiles:
self.add_item("All Music", "Play All Root Music", "")
for filePath in sorted(rootMusicFiles):
fileName = os.path.basename(filePath)
displayName = os.path.splitext(fileName)[0].replace('_', ' ')
command = f'mpv --no-video --really-quiet "{filePath}"'
self.add_item("All Music", displayName, command)
if not self.sectionNames:
self.add_section("Music Library")
self.add_item("Music Library", "No music found", "")
def run(self):
if not self.sectionNames:
message = "Menu is empty. No music folders found. Exiting."
print(message)
self.init_speech()
if self.speechClient:
self.speak(message)
time.sleep(3)
self.cleanup(fullCleanup=True)
sys.exit(0)
if not self.hasItems:
message = "No music files found in any sections. Exiting."
print(message)
self.init_speech()
if self.speechClient:
self.speak(message)
time.sleep(3)
self.cleanup(fullCleanup=True)
sys.exit(0)
try:
self.stdscr = curses.initscr()
self.cursesInitialized = True
curses.noecho()
curses.cbreak()
self.stdscr.keypad(True)
self.draw_menu()
self.speak("Music Player")
time.sleep(1)
self.announce_current_section(interrupt=False)
time.sleep(0.5)
self.announce_current_item(interrupt=False)
while True:
key = self.stdscr.getch()
self.stop_speech()
if key == curses.KEY_UP:
items = self.get_current_items()
if items:
index = self.get_current_item_index()
self.set_current_item_index((index - 1) % len(items))
self.draw_menu()
self.announce_current_item()
elif key == curses.KEY_DOWN:
items = self.get_current_items()
if items:
index = self.get_current_item_index()
self.set_current_item_index((index + 1) % len(items))
self.draw_menu()
self.announce_current_item()
elif key == curses.KEY_LEFT:
if self.currentView == "main":
if self.sectionNames and len(self.sectionNames) > 1:
try:
self.currentSection = (self.currentSection - 1) % len(self.sectionNames)
self.draw_menu()
self.announce_current_section()
time.sleep(0.5)
self.announce_current_item(interrupt=False)
except Exception as e:
print(f"Error navigating: {e}")
elif key == curses.KEY_RIGHT:
if self.currentView == "main":
if self.sectionNames and len(self.sectionNames) > 1:
try:
self.currentSection = (self.currentSection + 1) % len(self.sectionNames)
self.draw_menu()
self.announce_current_section()
time.sleep(0.5)
self.announce_current_item(interrupt=False)
except Exception as e:
print(f"Error navigating: {e}")
elif key == curses.KEY_ENTER or key == 10 or key == 13:
items = self.get_current_items()
if items:
index = self.get_current_item_index()
if 0 <= index < len(items):
name, command, isDirectory, directoryPath = items[index]
if self.currentView == "album" and name == "Back to Artist":
self.go_back()
else:
self.execute_current_item()
elif key == curses.KEY_BACKSPACE or key == 8 or key == 127:
if self.currentView != "main":
self.go_back()
elif key == ord('r') or key == ord('R'):
self.toggle_randomize()
self.draw_menu()
elif key == ord('h') or key == ord('H'):
self.speak_help()
elif key == ord('['):
self.decrease_speech_rate()
self.draw_menu()
elif key == ord(']'):
self.increase_speech_rate()
self.draw_menu()
elif key == 27 or key == ord('q') or key == ord('Q'):
break
except Exception as e:
if self.cursesInitialized:
try:
curses.endwin()
except:
pass
print(f"An error occurred: {e}")
finally:
self.cleanup(fullCleanup=True)
if __name__ == "__main__":
player = VoicedMusicPlayer(title="Stormux Music Player")
player.load_music_from_directory("~/Music")
player.run()
-138
View File
@@ -1,138 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Simple OCR Screen Reader
A lightweight tool that performs OCR on the screen and speaks the results
Optimized for Arch Linux ARM on Raspberry Pi with DWM
"""
import os
import sys
import time
import subprocess
from PIL import Image, ImageOps
import pytesseract
def capture_screen(max_retries=3, initial_delay=0.2):
"""
Capture the screen using scrot with robust checking and retries
Args:
max_retries: Maximum number of attempts to read the image
initial_delay: Initial delay in seconds (will increase with retries)
"""
temp_file = "/tmp/ocr_capture.png"
try:
# Capture the screen
subprocess.run(["scrot", temp_file], check=True)
# Wait and retry approach with validity checking
delay = initial_delay
for attempt in range(max_retries):
time.sleep(delay)
# Check if file exists and has content
if os.path.exists(temp_file) and os.path.getsize(temp_file) > 0:
try:
# Try to verify the image is valid
with Image.open(temp_file) as test_img:
# Just accessing a property forces PIL to validate the image
test_img.size
# If we get here, the image is valid
return Image.open(temp_file)
except (IOError, OSError) as e:
# Image exists but isn't valid yet
if attempt < max_retries - 1:
# Increase delay exponentially for next attempt
delay *= 2
continue
else:
raise Exception(f"Image file exists but is not valid after {max_retries} attempts")
# File doesn't exist or is empty
if attempt < max_retries - 1:
# Increase delay exponentially for next attempt
delay *= 2
else:
raise Exception(f"Screenshot file not created properly after {max_retries} attempts")
except Exception as e:
print(f"Error capturing screen: {e}")
raise
finally:
# Ensure file is removed even if an error occurs
if os.path.exists(temp_file):
os.remove(temp_file)
def process_image(img, scale_factor=1.5):
"""Process the image to improve OCR accuracy"""
# Scale the image to improve OCR
if scale_factor != 1:
width, height = img.size
img = img.resize((int(width * scale_factor), int(height * scale_factor)),
Image.Resampling.BICUBIC)
# Convert to grayscale for faster processing
img = ImageOps.grayscale(img)
# Improve contrast for better text recognition
img = ImageOps.autocontrast(img)
return img
def perform_ocr(img, lang='eng'):
"""Perform OCR on the image"""
# Use tessaract with optimized settings
# --oem 1: Use LSTM OCR Engine
# --psm 6: Assume a single uniform block of text
text = pytesseract.image_to_string(img, lang=lang, config='--oem 1 --psm 6')
return text
def speak_text(text):
"""Speak the text using speech-dispatcher"""
# Filter out empty lines and clean up the text
lines = [line.strip() for line in text.split('\n') if line.strip()]
cleaned_text = ' '.join(lines)
# Use speech-dispatcher to speak the text
if cleaned_text:
subprocess.run(["spd-say", "-Cw", cleaned_text])
else:
subprocess.run(["spd-say", "-Cw", "No text detected"])
def main():
# Limit tesseract thread usage to improve performance on Pi
os.environ["OMP_THREAD_LIMIT"] = "4"
try:
# Announce start
subprocess.run(["spd-say", "-Cw", "performing OCR"])
# Capture screen
img = capture_screen()
# Process image
processed_img = process_image(img, scale_factor=1.5)
# Perform OCR
text = perform_ocr(processed_img)
# Speak the results
speak_text(text)
except Exception as e:
# Let the user know something went wrong
error_msg = f"Error during OCR: {str(e)}"
print(error_msg)
try:
subprocess.run(["spd-say", "-Cw", "OCR failed"])
except:
# If even speech fails, at least we tried
pass
if __name__ == "__main__":
main()
-27
View File
@@ -1,27 +0,0 @@
#!/usr/bin/env bash
recordingDir=~/Audio
mkdir -p "$recordingDir"
pidFile="/tmp/game_recording.pid"
if [[ -f "$pidFile" ]]; then
pid=$(cat "$pidFile")
if ps -p "$pid" > /dev/null 2>&1; then
kill "$pid"
spd-say -Cw "Recording stopped"
play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 reverse
else
spd-say -Cw "Recording process not found, cleaning up"
fi
rm "$pidFile"
else
spd-say -Cw "Recording starting in"
for i in {3..1}; do
spd-say -Cw "$i"
sleep 0.5
done
play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20
ffmpeg -f pulse -i "$(pactl get-default-sink).monitor" "$recordingDir/game_$(date +%F_%H-%M-%S).ogg" &
echo "$!" > "$pidFile"
fi
-507
View File
@@ -1,507 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Self-voiced Terminal Menu ROM launcher
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
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
self.curses_initialized = False # Flag to track if curses has been initialized
self.has_items = False # Flag to track if any section has items
# 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)
# Load settings
self.load_settings()
# Initialize speech client
self.speechClient = None
self.init_speech()
def init_speech(self):
"""Initialize the speech client"""
try:
# Use a fixed client ID
self.speechClient = speechd.SSIPClient("rom_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)
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)
# 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 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))
self.has_items = True # Mark that we have at least one item
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"""
if not self.sectionNames:
return []
currentSectionName = self.sectionNames[self.currentSection]
return self.menuSections[currentSectionName]
def get_current_item_index(self):
"""Get the current item index in the current section"""
if not self.sectionNames:
return 0
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"""
if not self.sectionNames:
return
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]
# Clean up resources before executing the command
self.cleanup(full_cleanup=True) # This handles curses properly
# Execute the command and exit
os.system(command)
sys.exit(0) # Now safe to exit
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.
Escape or 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 = "Ãavigate | Enter: Select | H: Help | [ ] Rate | Q/Esc: Quit"
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()
if not items:
# Display a message if the section is empty
emptyMsg = "No items in this section"
x = max(0, w // 2 - len(emptyMsg) // 2)
self.stdscr.addstr(7, x, emptyMsg, curses.A_DIM)
else:
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)
self.stdscr.refresh()
def cleanup(self, full_cleanup=False):
"""Clean up resources before exiting or executing a command
Args:
full_cleanup: If True, also close curses. Used when exiting or running a command.
"""
# Stop any speech
self.stop_speech()
# Close speech client
if self.speechClient:
try:
self.speechClient.close()
except:
pass
self.speechClient = None
# Restore terminal settings if curses was initialized
if full_cleanup and self.curses_initialized:
try:
curses.nocbreak()
self.stdscr.keypad(False)
curses.echo()
curses.endwin()
except:
# If there's an error, just try a simple endwin
try:
curses.endwin()
except:
pass # Last resort, just continue
def load_roms_from_directory(self, directoryPath):
"""Load ROMs from the specified directory and add them to the menu"""
# Expand the path (in case it contains ~)
directoryPath = os.path.expanduser(directoryPath)
# Check if directory exists
if not os.path.exists(directoryPath) or not os.path.isdir(directoryPath):
print(f"Directory {directoryPath} does not exist or is not a directory")
return
# Get all subdirectories in the roms directory
try:
subdirs = [d for d in os.listdir(directoryPath) if os.path.isdir(os.path.join(directoryPath, d))]
# For each subdirectory, create a section
for subdir in subdirs:
sectionPath = os.path.join(directoryPath, subdir)
# Get all files in the subdirectory
files = [f for f in os.listdir(sectionPath) if os.path.isfile(os.path.join(sectionPath, f))]
# If the directory has files, add it as a section
if files:
# Add the section
self.add_section(subdir)
# Add files as menu items
for file in files:
# Get full path to the file
filePath = os.path.join(sectionPath, file)
# Create display name - remove extension
displayName = os.path.splitext(file)[0]
# Replace underscores with spaces for better readability
displayName = displayName.replace('_', ' ')
# Properly escape special characters in file path
escapedPath = filePath.replace('"', '\\"')
# Add the item to the section - use double quotes for the GAME variable
self.add_item(subdir, displayName, f'export GAME="{escapedPath}" && startx')
except Exception as e:
print(f"Error loading ROMs directory: {e}")
def run(self):
"""Run the menu system"""
# Check if menu is completely empty
if not self.sectionNames:
message = "No games found."
print(message)
# Speak the message
self.init_speech() # Make sure speech is initialized
if self.speechClient:
self.speak(message)
# Wait for speech to finish (rough estimate)
time.sleep(3)
# Clean up and exit properly
self.cleanup(full_cleanup=True)
sys.exit(0)
# Check if any sections have items
if not self.has_items:
message = "No ROMs found in any sections. Exiting."
print(message)
# Speak the message
self.init_speech() # Make sure speech is initialized
if self.speechClient:
self.speak(message)
# Wait for speech to finish (rough estimate)
time.sleep(3)
# Clean up and exit properly
self.cleanup(full_cleanup=True)
sys.exit(0)
try:
# Initialize curses
self.stdscr = curses.initscr()
self.curses_initialized = True # Mark curses as initialized
curses.noecho() # Don't echo keypresses
curses.cbreak() # React to keys instantly
self.stdscr.keypad(True) # Enable special keys
# Initial draw
self.draw_menu()
# Welcome message - don't interrupt this initial speech
self.speak("Roms menu")
# 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:
index = self.get_current_item_index()
self.set_current_item_index((index - 1) % len(items))
self.draw_menu()
self.announce_current_item()
elif key == curses.KEY_DOWN:
# Move to next item in current section
items = self.get_current_items()
if items:
index = self.get_current_item_index()
self.set_current_item_index((index + 1) % len(items))
self.draw_menu()
self.announce_current_item()
elif key == curses.KEY_LEFT:
# Move to previous section
if self.sectionNames:
self.currentSection = (self.currentSection - 1) % len(self.sectionNames)
self.draw_menu()
# 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:
self.currentSection = (self.currentSection + 1) % len(self.sectionNames)
self.draw_menu()
# 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
items = self.get_current_items()
if items: # Only execute if there are items
self.execute_current_item() # This now handles cleanup and exit
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 == 27 or key == ord('q') or key == ord('Q'): # Esc or Q
break
except Exception as e:
# End curses in case of error
if self.curses_initialized:
try:
curses.endwin()
except:
pass
print(f"An error occurred: {e}")
finally:
# Clean up - safe to call even if curses wasn't initialized
self.cleanup(full_cleanup=True)
# Example usage
if __name__ == "__main__":
# Create the menu with sections
menu = VoicedMenu(title="")
# Load ROMs from the ~/Roms directory
menu.load_roms_from_directory("~/Roms")
# Run the menu
menu.run()
-96
View File
@@ -1,96 +0,0 @@
#!/bin/bash
# Script to select an audio file and convert it using convert-to-video.sh
# Accessibility notes: Uses yad with titles, text labels, and standard GTK keyboard navigation.
AUDIO_DIR="$HOME/Audio"
CONVERTER="/usr/local/bin/convert-to-video.sh"
# Check if Audio directory exists
if [[ ! -d "$AUDIO_DIR" ]]; then
echo "The directory $AUDIO_DIR does not exist." |
yad --text-info \
--title="Error" \
--show-cursor \
--button="OK:0" \
--center
exit 1
fi
# Find audio files (mp3, ogg, opus, wav, flac) case-insensitive
# -maxdepth 1: Only look in the top level of the directory
# -printf "%f\n": Print only the filename followed by a newline
FILES=$(find "$AUDIO_DIR" -maxdepth 1 -type f \( -iname "*.mp3" -o -iname "*.ogg" -o -iname "*.opus" -o -iname "*.wav" -o -iname "*.flac" \) -printf "%f\n" | sort)
# Check if any files were found
if [[ -z "$FILES" ]]; then
echo "No audio files available in $AUDIO_DIR." |
yad --text-info \
--title="No Audio Files" \
--show-cursor \
--button="OK:0" \
--center \
--width=300
exit 0
fi
# Display selection dialog
# --list: Create a list box
# --column="File": Single column with header
# --separator="": Return exact value without extra separators
# --search-column=1: Allows typing to jump to files (accessibility win)
SELECTED_FILE=$(echo "$FILES" | yad --list \
--title="Select Audio File" \
--text="Select an audio file to convert:" \
--column="File" \
--separator="" \
--width=600 \
--height=500 \
--center \
--search-column=1)
# Check exit status
# 0 = OK/Enter
# Any other value (1, 252) = Cancel or Close
RET=$?
# If user canceled or selection is empty, exit silently
if [[ $RET -ne 0 ]] || [[ -z "$SELECTED_FILE" ]]; then
exit 0
fi
# Construct full path
INPUT_FILE="$AUDIO_DIR/$SELECTED_FILE"
# Run the conversion
# We use a subshell to run the command and pipe output to yad
(
echo "# Converting: $SELECTED_FILE"
# Run the converter script
"$CONVERTER" "$INPUT_FILE"
# Send 100% to yad to close the progress bar (due to --auto-close)
echo "100"
) | yad --progress \
--title="Converting" \
--text="Starting conversion for $SELECTED_FILE... This can take a long time." \
--pulsate \
--auto-close \
--auto-kill \
--center \
--width=500 \
--button="Cancel:1"
# Check if the process completed successfully (yad returns 0 on auto-close/success)
PROGRESS_RET=$?
if [[ $PROGRESS_RET -eq 0 ]]; then
echo "Transcode complete." |
yad --text-info \
--title="Success" \
--show-cursor \
--button="OK:0" \
--center \
--width=300
fi
exit 0
-52
View File
@@ -1,52 +0,0 @@
#!/usr/bin/env bash
# Timezone configuration for Stormux Gaming Image
# Uses the same interface as standard Stormux images
export DIALOGOPTS='--insecure --no-lines --visit-items'
set_timezone() {
# Get the list of timezones
mapfile -t regions < <(timedatectl --no-pager list-timezones | cut -d '/' -f1 | sort -u)
# Use the same text twice here and just hide the tag field.
region=$(dialog --backtitle "Please select your Region" \
--no-tags \
--menu "Use up and down arrows or page-up and page-down to navigate the list, and press 'Enter' to make your selection." 0 0 0 \
$(for i in ${regions[@]} ; do echo "$i";echo "$i";done) --stdout)
if [[ -z "$region" ]]; then
echo "No region selected"
exit 1
fi
mapfile -t cities < <(timedatectl --no-pager list-timezones | grep "$region" | cut -d '/' -f2 | sort -u)
# Use the same text twice here and just hide the tag field.
city=$(dialog --backtitle "Please select a city near you" \
--no-tags \
--menu "Use up and down arrow or page-up and page-down to navigate the list." 0 0 10 \
$(for i in ${cities[@]} ; do echo "$i";echo "$i";done) --stdout)
if [[ -z "$city" ]]; then
echo "No city selected"
exit 1
fi
# Set the timezone
if [[ -f /etc/localtime ]]; then
rm /etc/localtime
fi
ln -sf /usr/share/zoneinfo/${region}/${city} /etc/localtime
timedatectl set-ntp true
echo "Timezone set to ${region}/${city}"
}
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo "This script requires root privileges"
exit 1
fi
set_timezone
-387
View File
@@ -1,387 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Self-voiced Terminal Menu for Speech Dispatcher Voice Selection
import os
import sys
import time
import curses
import speechd
import subprocess
import configparser
class VoiceSelectionMenu:
def __init__(self, title="Speech Dispatcher Voice Selection"):
self.title = title
self.voice_modules = [] # List to store available voice modules
self.current_index = 0 # Index of current selection
self.stdscr = None
self.curses_initialized = False # Flag to track if curses has been initialized
# Initialize speech client
self.speech_client = None
self.init_speech()
# Load available voice modules
self.load_voice_modules()
def init_speech(self):
"""Initialize the speech client"""
try:
self.speech_client = speechd.SSIPClient("voice_selection_menu")
self.speech_client.set_priority(speechd.Priority.IMPORTANT)
self.speech_client.set_punctuation(speechd.PunctuationMode.SOME)
except Exception as e:
print(f"Could not initialize speech: {e}")
# Fallback to None - the speak method will handle this
def load_voice_modules(self):
"""Load available speech-dispatcher modules"""
try:
# Execute the command to get available output modules
result = subprocess.run(['spd-say', '-O'],
capture_output=True,
text=True,
check=True)
# Process the output to get the list of modules
lines = result.stdout.strip().split('\n')
# Skip the first line (header)
modules = [line.strip() for line in lines[1:] if line.strip()]
# Store the modules
self.voice_modules = modules
if not modules:
print("No speech-dispatcher modules found.")
except Exception as e:
print(f"Error loading voice modules: {e}")
self.voice_modules = []
def speak(self, text, interrupt=True, module=None):
"""Speak the given text with option to interrupt existing speech"""
if self.speech_client is None:
return
try:
if interrupt:
self.stop_speech()
# If a specific module is requested, try to use it
if module:
current_module = self.speech_client.get_output_module()
self.speech_client.set_output_module(module)
self.speech_client.speak(text)
# Restore previous module after speaking
self.speech_client.set_output_module(current_module)
else:
# Use default module
self.speech_client.speak(text)
except Exception as e:
# If speech fails, try to reinitialize and try once more
try:
self.init_speech()
if self.speech_client:
self.speech_client.speak(text)
except:
# If reinitializing fails, just give up silently
pass
def stop_speech(self):
"""Stop any ongoing speech"""
if self.speech_client is None:
return
try:
self.speech_client.cancel()
except Exception as e:
# If cancel fails, try to reinitialize
self.init_speech()
def announce_current_item(self, interrupt=True):
"""Announce the currently selected voice module"""
if self.voice_modules and 0 <= self.current_index < len(self.voice_modules):
module = self.voice_modules[self.current_index]
self.speak(f"Module {module}", interrupt=interrupt)
def test_selected_module(self):
"""Test the currently selected voice module"""
if self.voice_modules and 0 <= self.current_index < len(self.voice_modules):
module = self.voice_modules[self.current_index]
test_message = f"This is a test of the {module} speech-dispatcher module. If you can hear this message, press enter to set {module} as your default module. If enter is not pressed within 15 seconds, no changes will be made to your system."
# Speak using the selected module - should not be interrupted
self.speak(test_message, interrupt=False, module=module)
# Draw a message instructing the user to press Enter
h, w = self.stdscr.getmaxyx()
confirm_msg = "Press ENTER within 15 seconds to confirm selection or any other key to cancel."
x = max(0, w // 2 - len(confirm_msg) // 2)
self.stdscr.addstr(h-2, x, confirm_msg, curses.A_BOLD)
self.stdscr.refresh()
# Wait for user confirmation with timeout
self.stdscr.timeout(15000) # 15 seconds timeout
key = self.stdscr.getch()
self.stdscr.timeout(-1) # Reset timeout
# Check if Enter was pressed
if key == curses.KEY_ENTER or key == 10 or key == 13:
return True
else:
self.speak("Confirmation not received, no changes made to your speech-dispatcher configuration.", interrupt=False)
return False
return False
def set_default_module(self):
"""Set the selected module as the default speech-dispatcher module"""
if self.voice_modules and 0 <= self.current_index < len(self.voice_modules):
module = self.voice_modules[self.current_index]
# Test the module first
if not self.test_selected_module():
return
try:
# Clean up before executing system commands
self.cleanup(full_cleanup=False)
# Read the current config file
import re
with open('/etc/speech-dispatcher/speechd.conf', 'r') as f:
content = f.read()
# Check if DefaultModule is already uncommented
if re.search(r'^\s*DefaultModule\s+', content, re.MULTILINE):
# Replace existing DefaultModule line
new_content = re.sub(
r'^(\s*)DefaultModule\s+\S+',
f'\\1DefaultModule {module}',
content,
flags=re.MULTILINE
)
else:
# Uncomment and set DefaultModule line
new_content = re.sub(
r'^(\s*)#\s*DefaultModule\s+\S*',
f'\\1DefaultModule {module}',
content,
flags=re.MULTILINE
)
# Write to a temporary file
temp_file = "/tmp/speechd.conf.new"
with open(temp_file, 'w') as f:
f.write(new_content)
# Use sudo to move the file to the correct location
subprocess.run(f"sudo mv {temp_file} /etc/speech-dispatcher/speechd.conf", shell=True, check=True)
# Restart speech-dispatcher more thoroughly
subprocess.run("sudo systemctl restart speech-dispatcher", shell=True, check=False)
# Also kill any remaining processes
subprocess.run("sudo killall speech-dispatcher", shell=True, check=False)
# Re-initialize speech after changes
time.sleep(2) # Give more time for the service to restart
self.init_speech()
# Notify the user that the change is complete - should not be interrupted
self.speak(f"The {module} module is now set as the default voice for this system.", interrupt=False)
# Return to the menu after speech finishes
# No sleep here - the next UI repaint doesn't depend on speech finishing
self.draw_menu()
except Exception as e:
print(f"Error setting default module: {e}")
self.speak("An error occurred while attempting to set the default module.", interrupt=False)
def speak_help(self):
"""Speak help information"""
helpText = """
Navigation controls:
Up arrow: Previous voice module.
Down arrow: Next voice module.
Enter: Test and set the selected voice module.
H key: Hear these instructions again.
Escape or Q: Exit the menu.
Any key will interrupt speech.
"""
self.speak(helpText, interrupt=False)
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 = "Up/Down: Navigate | Enter: Test & Set | H: Help | Q/Esc: Quit"
x = max(0, w // 2 - len(helpText) // 2)
self.stdscr.addstr(3, x, helpText)
# Check if we have items
if not self.voice_modules:
message = "No speech-dispatcher modules found."
x = max(0, w // 2 - len(message) // 2)
self.stdscr.addstr(5, x, message, curses.A_DIM)
else:
# Show a limited number of items, centered around the current selection
max_display = min(h - 7, len(self.voice_modules)) # Max number of items to display
# Calculate starting index for display
half_display = max_display // 2
if self.current_index < half_display:
start_idx = 0
elif self.current_index >= len(self.voice_modules) - half_display:
start_idx = max(0, len(self.voice_modules) - max_display)
else:
start_idx = self.current_index - half_display
# Draw visible menu items
for i in range(start_idx, min(start_idx + max_display, len(self.voice_modules))):
y = (i - start_idx) + 5 # Start items at line 5
# Highlight the selected item
module = self.voice_modules[i]
if i == self.current_index:
text = f" > {module} "
attr = curses.A_REVERSE
else:
text = f" {module} "
attr = curses.A_NORMAL
x = max(0, w // 2 - len(text) // 2)
self.stdscr.addstr(y, x, text, attr)
self.stdscr.refresh()
def cleanup(self, full_cleanup=False):
"""Clean up resources before exiting or executing a command
Args:
full_cleanup: If True, also close curses. Used when exiting.
"""
# Stop any speech
self.stop_speech()
# Close speech client
if self.speech_client:
try:
self.speech_client.close()
except:
pass
self.speech_client = None
# Restore terminal settings if curses was initialized
if full_cleanup and self.curses_initialized:
try:
curses.nocbreak()
self.stdscr.keypad(False)
curses.echo()
curses.endwin()
except:
# If there's an error, just try a simple endwin
try:
curses.endwin()
except:
pass # Last resort, just continue
def run(self):
"""Run the menu system"""
# Check if menu is empty or only has one item
if not self.voice_modules:
message = "No speech-dispatcher modules found. Exiting."
print(message)
# Clean up and exit properly
self.cleanup(full_cleanup=True)
sys.exit(0)
elif len(self.voice_modules) == 1:
message = f"{self.voice_modules[0]} is the only available module and is already set for this system."
print(message)
# Speak the message
self.init_speech()
if self.speech_client:
# Use speech_client.speak directly with wait flag to ensure it completes
self.speech_client.speak(message)
# Wait for speech to complete
self.speech_client.close()
# Clean up and exit properly
self.cleanup(full_cleanup=True)
sys.exit(0)
try:
# Initialize curses
self.stdscr = curses.initscr()
self.curses_initialized = True
curses.noecho()
curses.cbreak()
self.stdscr.keypad(True)
# Initial draw
self.draw_menu()
# Welcome message
self.speak(self.title, interrupt=False)
# Announce first item after welcome finishes
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
self.current_index = (self.current_index - 1) % len(self.voice_modules)
self.draw_menu()
self.announce_current_item()
elif key == curses.KEY_DOWN:
# Move to next item
self.current_index = (self.current_index + 1) % len(self.voice_modules)
self.draw_menu()
self.announce_current_item()
elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key
self.set_default_module()
elif key == ord('h') or key == ord('H'): # Help
self.speak_help()
elif key == 27 or key == ord('q') or key == ord('Q'): # Esc or Q
break
except Exception as e:
# End curses in case of error
if self.curses_initialized:
try:
curses.endwin()
except:
pass
print(f"An error occurred: {e}")
finally:
# Clean up - safe to call even if curses wasn't initialized
self.cleanup(full_cleanup=True)
# Run the menu
if __name__ == "__main__":
# Create the menu
menu = VoiceSelectionMenu()
# Run the menu
menu.run()
-438
View File
@@ -1,438 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Self-voiced Speech Rate Configuration Menu
import os
import sys
import time
import curses
import speechd # Python bindings for Speech Dispatcher
import re
import subprocess
def update_speechd_config_content(content, rate, volume, pitch):
"""Return speechd.conf content with updated default voice parameters."""
newContent = content
replacements = (
("DefaultRate", rate),
("DefaultVolume", volume),
("DefaultPitch", pitch),
)
for settingName, settingValue in replacements:
activePattern = rf'^(\s*){settingName}\s+(-?\d+)'
commentedPattern = rf'^(\s*)#\s*{settingName}\s+(-?\d+)'
replacement = rf'\1{settingName} {settingValue}'
if re.search(activePattern, newContent, re.MULTILINE):
newContent = re.sub(
activePattern,
replacement,
newContent,
flags=re.MULTILINE,
)
else:
newContent = re.sub(
commentedPattern,
replacement,
newContent,
flags=re.MULTILINE,
)
return newContent
def get_writable_config_targets(configTargets):
"""Return existing config paths to update, preserving order."""
return [configPath for configPath in configTargets if os.path.exists(configPath)]
class SpeechRateMenu:
def __init__(self, title="Speech Configuration"):
self.title = title
self.currentRate = 0 # Default rate
self.currentVolume = 100 # Default volume
self.currentPitch = 0 # Default pitch
self.currentMode = 0 # 0=Rate, 1=Volume, 2=Pitch
self.modes = ["Rate", "Volume", "Pitch"]
self.stdscr = None
self.cursesInitialized = False # Flag to track if curses has been initialized
self.configFile = "/etc/speech-dispatcher/speechd.conf"
self.configTargets = [
self.configFile,
os.path.expanduser("~/.fex-emu/RootFS/ArchLinux/etc/speech-dispatcher/speechd.conf"),
]
# Load current settings from config FIRST
self.load_current_settings()
# Initialize speech client AFTER loading the settings
self.speechClient = None
self.init_speech()
def init_speech(self):
"""Initialize the speech client"""
try:
self.speechClient = speechd.SSIPClient("speech_config_menu")
self.speechClient.set_priority(speechd.Priority.IMPORTANT)
self.speechClient.set_punctuation(speechd.PunctuationMode.SOME)
# Apply the loaded settings to the speech client
self.speechClient.set_rate(self.currentRate)
self.speechClient.set_volume(self.currentVolume)
self.speechClient.set_pitch(self.currentPitch)
except Exception as e:
# Fallback to None - the speak method will handle this
pass
def load_current_settings(self):
"""Load the current default settings from speechd.conf"""
try:
with open(self.configFile, 'r') as f:
content = f.read()
# Load Rate
activeMatch = re.search(r'^\s*DefaultRate\s+(-?\d+)', content, re.MULTILINE)
if activeMatch:
self.currentRate = int(activeMatch.group(1))
else:
commentedMatch = re.search(r'^\s*#\s*DefaultRate\s+(-?\d+)', content, re.MULTILINE)
if commentedMatch:
self.currentRate = int(commentedMatch.group(1))
# Load Volume
activeMatch = re.search(r'^\s*DefaultVolume\s+(-?\d+)', content, re.MULTILINE)
if activeMatch:
self.currentVolume = int(activeMatch.group(1))
else:
commentedMatch = re.search(r'^\s*#\s*DefaultVolume\s+(-?\d+)', content, re.MULTILINE)
if commentedMatch:
self.currentVolume = int(commentedMatch.group(1))
# Load Pitch
activeMatch = re.search(r'^\s*DefaultPitch\s+(-?\d+)', content, re.MULTILINE)
if activeMatch:
self.currentPitch = int(activeMatch.group(1))
else:
commentedMatch = re.search(r'^\s*#\s*DefaultPitch\s+(-?\d+)', content, re.MULTILINE)
if commentedMatch:
self.currentPitch = int(commentedMatch.group(1))
except Exception:
# If loading fails, we'll use default values
pass
def save_settings_to_config(self):
"""Save the current settings to the speech-dispatcher config file"""
try:
for configPath in get_writable_config_targets(self.configTargets):
with open(configPath, 'r') as f:
content = f.read()
newContent = update_speechd_config_content(
content,
self.currentRate,
self.currentVolume,
self.currentPitch,
)
tempFile = f"/tmp/{os.path.basename(configPath)}.new"
with open(tempFile, 'w') as f:
f.write(newContent)
subprocess.run(
["sudo", "mv", tempFile, configPath],
check=True,
)
return True
except Exception:
return False
def get_current_value(self):
"""Get the current value for the active mode"""
if self.currentMode == 0: # Rate
return self.currentRate
elif self.currentMode == 1: # Volume
return self.currentVolume
else: # Pitch
return self.currentPitch
def adjust_current_value(self, amount):
"""Adjust the current value by the given amount"""
if self.currentMode == 0: # Rate
# Rate should be between -50 and 100
newValue = max(-50, min(100, self.currentRate + amount))
if newValue != self.currentRate:
self.currentRate = newValue
if self.speechClient:
try:
self.speechClient.set_rate(self.currentRate)
self.speak(f"Speech rate {self.currentRate}")
except Exception:
pass
elif self.currentMode == 1: # Volume
# Volume should be between -100 and 100
newValue = max(-100, min(100, self.currentVolume + amount))
if newValue != self.currentVolume:
self.currentVolume = newValue
if self.speechClient:
try:
self.speechClient.set_volume(self.currentVolume)
self.speak(f"Volume {self.currentVolume}")
except Exception:
pass
else: # Pitch
# Pitch should be between -100 and 100
newValue = max(-100, min(100, self.currentPitch + amount))
if newValue != self.currentPitch:
self.currentPitch = newValue
if self.speechClient:
try:
self.speechClient.set_pitch(self.currentPitch)
self.speak(f"Pitch {self.currentPitch}")
except Exception:
pass
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:
# 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:
# If cancel fails, try to reinitialize
self.init_speech()
def confirm_saved_settings(self):
"""Restart speechd, announce success, and wait for confirmation."""
subprocess.run(
["sudo", "killall", "speech-dispatcher"],
check=False,
)
time.sleep(1)
self.init_speech()
self.speak("Speech settings applied. Press Enter to continue.", interrupt=False)
while True:
key = self.stdscr.getch()
if key == curses.KEY_ENTER or key == 10 or key == 13:
break
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 = "Up/Down: Adjust | Tab: Switch Mode | Enter: Save | Q/Esc: Quit"
x = max(0, w // 2 - len(helpText) // 2)
self.stdscr.addstr(3, x, helpText)
# Draw all current values
currentMode = self.modes[self.currentMode]
# Rate display
rateText = f"Rate: {self.currentRate}"
attr = curses.A_REVERSE if self.currentMode == 0 else curses.A_NORMAL
x = max(0, w // 2 - 30)
self.stdscr.addstr(5, x, rateText, attr)
# Volume display
volumeText = f"Volume: {self.currentVolume}"
attr = curses.A_REVERSE if self.currentMode == 1 else curses.A_NORMAL
x = max(0, w // 2 - 5)
self.stdscr.addstr(5, x, volumeText, attr)
# Pitch display
pitchText = f"Pitch: {self.currentPitch}"
attr = curses.A_REVERSE if self.currentMode == 2 else curses.A_NORMAL
x = max(0, w // 2 + 20)
self.stdscr.addstr(5, x, pitchText, attr)
# Current mode indicator
modeText = f"Current Mode: {currentMode}"
x = max(0, w // 2 - len(modeText) // 2)
self.stdscr.addstr(7, x, modeText, curses.A_BOLD)
# Draw visualization bar for current parameter
barWidth = 50 # Width of the visualization bar
barX = max(0, w // 2 - barWidth // 2)
# Get current value and range
currentValue = self.get_current_value()
if self.currentMode == 0: # Rate
minVal, maxVal = -50, 100
totalRange = 150
normalizedValue = currentValue + 50
else: # Volume or Pitch
minVal, maxVal = -100, 100
totalRange = 200
normalizedValue = currentValue + 100
position = int((normalizedValue / totalRange) * barWidth)
# Draw the bar
barY = 9
self.stdscr.addstr(barY, barX, "" + "" * barWidth + "")
self.stdscr.addstr(barY + 1, barX, "" + " " * barWidth + "")
self.stdscr.addstr(barY + 2, barX, "" + "" * barWidth + "")
# Draw the position marker
if 0 <= position < barWidth:
self.stdscr.addstr(barY + 1, barX + 1 + position, "", curses.A_BOLD)
# Add labels for min and max
self.stdscr.addstr(barY + 3, barX, str(minVal))
maxLabel = str(maxVal)
self.stdscr.addstr(barY + 3, barX + barWidth - len(maxLabel), maxLabel)
# Note about saving
note = "Press Enter to save all settings to system config"
x = max(0, w // 2 - len(note) // 2)
self.stdscr.addstr(h - 3, x, note, curses.A_DIM)
# Warning about system config
warning = "Note: Saving requires sudo privileges"
x = max(0, w // 2 - len(warning) // 2)
self.stdscr.addstr(h - 2, x, warning, curses.A_DIM)
self.stdscr.refresh()
def cleanup(self, fullCleanup=False):
"""Clean up resources before exiting
Args:
fullCleanup: If True, also close curses. Used when 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 if curses was initialized
if fullCleanup and self.cursesInitialized:
try:
curses.nocbreak()
self.stdscr.keypad(False)
curses.echo()
curses.endwin()
except:
# If there's an error, just try a simple endwin
try:
curses.endwin()
except:
pass # Last resort, just continue
def run(self):
"""Run the menu system"""
try:
# Initialize curses
self.stdscr = curses.initscr()
self.cursesInitialized = True
curses.noecho()
curses.cbreak()
self.stdscr.keypad(True)
# Initial draw
self.draw_menu()
# Welcome message
currentMode = self.modes[self.currentMode]
self.speak(f"Speech configuration menu. Currently adjusting {currentMode}. Rate {self.currentRate}, Volume {self.currentVolume}, Pitch {self.currentPitch}.")
# 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:
# Increase current value by 10
self.adjust_current_value(10)
self.draw_menu()
elif key == curses.KEY_DOWN:
# Decrease current value by 10
self.adjust_current_value(-10)
self.draw_menu()
elif key == ord('\t') or key == 9: # Tab key
# Switch to next mode
self.currentMode = (self.currentMode + 1) % len(self.modes)
currentMode = self.modes[self.currentMode]
currentValue = self.get_current_value()
self.speak(f"Switching to {currentMode}. Current value: {currentValue}")
self.draw_menu()
elif key == curses.KEY_ENTER or key == 10 or key == 13: # Enter key
# Save all settings
self.speak("Saving speech settings to system configuration.")
success = self.save_settings_to_config()
if success:
self.confirm_saved_settings()
else:
self.speak("Failed to save speech settings. You may need root privileges.")
break # Exit the loop after saving
elif key == 27 or key == ord('q') or key == ord('Q'): # Esc or Q
self.speak("Speech settings discarded.")
time.sleep(3)
break
except Exception:
# End curses in case of error
if self.cursesInitialized:
try:
curses.endwin()
except:
pass
finally:
# Clean up - safe to call even if curses wasn't initialized
self.cleanup(fullCleanup=True)
# Run the menu
if __name__ == "__main__":
# Create the menu
menu = SpeechRateMenu()
# Run the menu
menu.run()
-257
View File
@@ -1,257 +0,0 @@
#!/usr/bin/env bash
# Stormux Installation Helper
# Provides automatic installation with speech feedback and progress indicators
# Configuration
GAMES_REGISTRY="/usr/share/stormux/downloadable_games.json"
PACKAGES_REGISTRY="/usr/share/stormux/installable_packages.json"
LOG_DIR="$HOME/Logs"
LOG_FILE="$LOG_DIR/installer.log"
INSTALL_BASE="$HOME/.local/games"
NVDA_DLL_SOURCE="$HOME/.local/games/nvda"
# Initialize logging
init_logging() {
mkdir -p "$LOG_DIR"
# Clear previous log and start fresh
echo "=== Installation started at $(date) ===" > "$LOG_FILE"
}
# Speech output
speak() {
local message="$1"
echo "$message" >> "$LOG_FILE"
spd-say "$message" 2>/dev/null || echo "$message"
}
# Progress beep
progress_beep() {
# Simple beep using speaker-test or paplay
if command -v paplay &> /dev/null && [[ -f /usr/share/sounds/freedesktop/stereo/message.oga ]]; then
paplay /usr/share/sounds/freedesktop/stereo/message.oga 2>/dev/null &
fi
}
# Download with progress feedback
download_with_progress() {
local url="$1"
local output="$2"
local name="$3"
speak "Downloading $name"
echo "Downloading from: $url" >> "$LOG_FILE"
# Start background process for progress beeps
(
while kill -0 $$ 2>/dev/null; do
sleep 5
progress_beep
done
) &
local beep_pid=$!
# Download with curl
if curl -L -f --progress-bar -o "$output" "$url" 2>> "$LOG_FILE"; then
kill $beep_pid 2>/dev/null
wait $beep_pid 2>/dev/null
echo "Download successful" >> "$LOG_FILE"
return 0
else
kill $beep_pid 2>/dev/null
wait $beep_pid 2>/dev/null
echo "Download failed" >> "$LOG_FILE"
return 1
fi
}
# Replace NVDA DLLs in game directory
replace_nvda_dlls() {
local game_dir="$1"
if [[ ! -d "$NVDA_DLL_SOURCE" ]]; then
echo "Warning: NVDA DLL source directory not found" >> "$LOG_FILE"
return 0
fi
# Replace all NVDA DLLs
{
find "$game_dir" -type f -name "nvdaControllerClient32.dll" -exec cp -v "$NVDA_DLL_SOURCE/nvdaControllerClient32.dll" '{}' \;
find "$game_dir" -type f -name "nvdaControllerClient64.dll" -exec cp -v "$NVDA_DLL_SOURCE/nvdaControllerClient64.dll" '{}' \;
find "$game_dir" -type f -name "nvdaControllerClient.dll" -exec cp -v "$NVDA_DLL_SOURCE/nvdaControllerClient64.dll" '{}' \;
} >> "$LOG_FILE" 2>&1
}
# Extract and setup game from ZIP
setup_game_zip() {
local zipfile="$1"
local game_dir="$2"
local game_name="$3"
speak "Installing $game_name"
echo "Extracting to: $game_dir" >> "$LOG_FILE"
# Remove old installation
rm -rf "$game_dir"
mkdir -p "$game_dir"
# Extract to temp location first to handle any ZIP structure
local temp_extract="/tmp/stormux_install_$$"
mkdir -p "$temp_extract"
if ! unzip -q "$zipfile" -d "$temp_extract" 2>> "$LOG_FILE"; then
echo "Extraction failed" >> "$LOG_FILE"
rm -rf "$temp_extract"
return 2
fi
# Check if everything extracted into a single subdirectory
local extracted_items
extracted_items=("$temp_extract"/*)
if [[ ${#extracted_items[@]} -eq 1 ]] && [[ -d "${extracted_items[0]}" ]]; then
# Single directory - move its contents to game_dir
echo "Single directory extracted, moving contents" >> "$LOG_FILE"
mv "${extracted_items[0]}"/* "$game_dir/" 2>> "$LOG_FILE"
else
# Multiple items or single file - move everything
echo "Multiple items extracted, moving all" >> "$LOG_FILE"
mv "$temp_extract"/* "$game_dir/" 2>> "$LOG_FILE"
fi
rm -rf "$temp_extract"
# Replace NVDA DLLs
replace_nvda_dlls "$game_dir"
# Clean up temp file
rm -f "$zipfile"
echo "Installation complete" >> "$LOG_FILE"
return 0
}
# Install system package via yay
install_system_package() {
local package="$1"
local name="$2"
speak "Installing $name"
echo "Installing package: $package" >> "$LOG_FILE"
if yay -Sy --noconfirm "$package" >> "$LOG_FILE" 2>&1; then
echo "Package installation successful" >> "$LOG_FILE"
return 0
else
echo "Package installation failed" >> "$LOG_FILE"
speak "Installation of $name failed. Check logs."
return 1
fi
}
# Check if game is installed
is_game_installed() {
local game_dir="$1"
local executable="$2"
[[ -e "$game_dir/$executable" ]]
}
# Check if package is installed
is_package_installed() {
local verify_command="$1"
eval "$verify_command" &>/dev/null
}
# Auto-install: Main function
# Usage: auto_install <item_id>
# Returns: 0 if ready to launch, non-zero on failure
auto_install() {
local item_id="$1"
init_logging
# Try to find in games registry
if [[ -f "$GAMES_REGISTRY" ]]; then
echo "Reading games registry: $GAMES_REGISTRY" >> "$LOG_FILE"
local game_info
game_info=$(jq -r ".downloadable_games[\"$item_id\"] // empty" "$GAMES_REGISTRY" 2>> "$LOG_FILE")
echo "Game info result: ${game_info:-empty}" >> "$LOG_FILE"
if [[ -n "$game_info" ]]; then
local name url directory executable game_dir
name=$(echo "$game_info" | jq -r '.name')
url=$(echo "$game_info" | jq -r '.url')
directory=$(echo "$game_info" | jq -r '.directory')
executable=$(echo "$game_info" | jq -r '.executable')
game_dir="$INSTALL_BASE/$directory"
# Check if already installed
if is_game_installed "$game_dir" "$executable"; then
echo "Game already installed: $name" >> "$LOG_FILE"
return 0
fi
# Download and install
local temp_zip="/tmp/${directory}_download.zip"
if download_with_progress "$url" "$temp_zip" "$name"; then
if setup_game_zip "$temp_zip" "$game_dir" "$name"; then
# Verify installation
if is_game_installed "$game_dir" "$executable"; then
speak "Installation complete"
return 0
else
speak "Installation failed. Executable not found."
echo "Error: Executable not found after installation: $executable" >> "$LOG_FILE"
return 3
fi
else
speak "Installation failed. Could not extract game."
return 2
fi
else
speak "Download failed. Check your internet connection."
return 1
fi
fi
fi
# Try to find in packages registry
if [[ -f "$PACKAGES_REGISTRY" ]]; then
local package_info
package_info=$(jq -r ".installable_packages[\"$item_id\"] // empty" "$PACKAGES_REGISTRY" 2>/dev/null)
if [[ -n "$package_info" ]]; then
local name package verify_command
name=$(echo "$package_info" | jq -r '.name')
package=$(echo "$package_info" | jq -r '.package')
verify_command=$(echo "$package_info" | jq -r '.verify_command')
# Check if already installed
if is_package_installed "$verify_command"; then
echo "Package already installed: $name" >> "$LOG_FILE"
return 0
fi
# Install package
if install_system_package "$package" "$name"; then
speak "Installation complete"
return 0
else
return 1
fi
fi
fi
# Item not found in any registry
if [[ ! -f "$GAMES_REGISTRY" ]] && [[ ! -f "$PACKAGES_REGISTRY" ]]; then
echo "Error: Registry files not found!" >> "$LOG_FILE"
echo " Expected: $GAMES_REGISTRY" >> "$LOG_FILE"
echo " Expected: $PACKAGES_REGISTRY" >> "$LOG_FILE"
speak "Error: Installation registry files not found. This may be a development system."
else
echo "Error: Item not found in registries: $item_id" >> "$LOG_FILE"
speak "Error: $item_id not found in installation registry"
fi
return 4
}
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env bash
date_time=$(curl -s http://worldtimeapi.org/api/ip | grep -oP '(?<="datetime":")[^"]*')
date -s "$date_time"