wolf: redesign app selection with numbered menu, ES-DE ROM dirs, and manage.sh apps command

- Drive picker: numbered list with UUID display, mounted + unmounted drives,
  explicit system-path exclusions (no regex bare-/ bug), skip whole disks
  with mounted partitions
- Pre-create ES-DE ROM system directories (~50 systems) so ES-DE detects
  all platforms on first launch
- App selection at install time: numbered menu for Steam, ES-DE, Lutris,
  RetroArch, Prism Launcher, Kodi, Firefox, Desktop
- Python catalog-driven TOML injector shared between install and manage.sh;
  supports update_mounts() to patch paths on re-run without duplicating entries
- manage.sh `apps` command replaces `add-apps` / `update-storage`: shows
  installed apps, interactive picker, updates game storage path from .env
- Game storage written to ~/docker/wolf/.env as GAME_STORAGE_DIR

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8ckUJQtj1pH8jtAddBDZs
This commit is contained in:
Claude
2026-06-20 04:41:59 +00:00
parent fa906186c3
commit 73a41e7252
+448 -220
View File
@@ -694,10 +694,84 @@ UDEV
log_success "Game storage: $GAME_STORAGE_DIR"
# Create the storage sub-directories
mkdir -p "$GAME_STORAGE_DIR/roms" "$GAME_STORAGE_DIR/steam" \
"$GAME_STORAGE_DIR/saves" "$GAME_STORAGE_DIR/media"
mkdir -p "$GAME_STORAGE_DIR/steam" "$GAME_STORAGE_DIR/saves" \
"$GAME_STORAGE_DIR/media" "$GAME_STORAGE_DIR/lutris" \
"$GAME_STORAGE_DIR/firefox" "$GAME_STORAGE_DIR/minecraft" \
"$GAME_STORAGE_DIR/kodi"
# Pre-create ES-DE ROM directories so the user knows where to drop files
# and ES-DE shows the system in its list immediately on first launch.
local _ESDE_SYSTEMS=(
3do amstradcpc arcade atari2600 atari5200 atari7800 atari800
atarijaguar atarilynx atarist c64 cavestory channelf coco colecovision
dreamcast dos famicom fds gamegear gb gba gbc gc genesis
gx4000 intellivision j2me lynx mame megadrive megadrive-japan
msx msx2 n64 naomi nds neogeo neogeocd ngp ngpc nes nintendo3ds
odyssey2 pc88 pc98 pcengine pcenginecd pcfx pokemini ps2 ps3 psp psx
saturn scummvm sega32x segacd sg-1000 snes snesmsu1 supervision
switch tg16 tg-cd vectrex vic20 videopac wii wiiu wonderswan
wonderswancolor x68000 xbox xbox360 zmachine zxspectrum
)
local _sys
for _sys in "${_ESDE_SYSTEMS[@]}"; do
mkdir -p "$GAME_STORAGE_DIR/roms/$_sys"
done
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$GAME_STORAGE_DIR" 2>/dev/null || true
log_success "Storage layout: $GAME_STORAGE_DIR/{roms,steam,saves,media}"
log_success "Storage layout: $GAME_STORAGE_DIR/{roms/<system>/,steam,saves,media,...}"
log_info "ES-DE ROM directories pre-created. Drop ROMs in the matching subfolder."
# ── App selection ─────────────────────────────────────────────────────────
echo ""
echo "═══════════════════════════════════════════════════════"
echo " WOLF APPS"
echo "═══════════════════════════════════════════════════════"
echo ""
echo " Select apps to add to Wolf (shown in Moonlight)."
echo " Containers are pulled on first launch, not now."
echo ""
echo " 1) Steam - Big Picture + Proton (PC games)"
echo " 2) EmulationStation - ES-DE + RetroArch (retro ROMs) [default]"
echo " 3) Lutris - GOG / Epic / Wine / non-Steam"
echo " 4) RetroArch - standalone emulator frontend"
echo " 5) Prism Launcher - Minecraft (Java + Bedrock)"
echo " 6) Kodi - media center"
echo " 7) Firefox - browser"
echo " 8) Desktop - full XFCE desktop session"
echo ""
echo " Enter numbers separated by spaces, or 'all', or Enter for [1 2]:"
echo ""
local _APP_PICKS="" _SELECTED_APPS=()
if [ "$UNATTENDED" = true ]; then
_SELECTED_APPS=(1 2)
log_info "Unattended — selecting Steam + EmulationStation"
else
read -r -p " Apps [1 2]: " _APP_PICKS
_APP_PICKS="${_APP_PICKS:-1 2}"
if [[ "${_APP_PICKS,,}" == "all" ]]; then
_SELECTED_APPS=(1 2 3 4 5 6 7 8)
else
read -ra _SELECTED_APPS <<< "$_APP_PICKS"
fi
fi
# Map numbers to app keys for the Python injector
local _APP_KEYS=""
for _n in "${_SELECTED_APPS[@]}"; do
case "$_n" in
1) _APP_KEYS="$_APP_KEYS steam" ;;
2) _APP_KEYS="$_APP_KEYS esde" ;;
3) _APP_KEYS="$_APP_KEYS lutris" ;;
4) _APP_KEYS="$_APP_KEYS retroarch" ;;
5) _APP_KEYS="$_APP_KEYS prismlauncher" ;;
6) _APP_KEYS="$_APP_KEYS kodi" ;;
7) _APP_KEYS="$_APP_KEYS firefox" ;;
8) _APP_KEYS="$_APP_KEYS desktop" ;;
esac
done
_APP_KEYS="${_APP_KEYS# }" # trim leading space
log_info "Will add: ${_APP_KEYS:-none}"
# ── docker-compose.yml ────────────────────────────────────────────────────
log_info "Generating docker-compose.yml..."
@@ -803,45 +877,208 @@ case "$1" in
docker compose pull
docker compose up -d
;;
add-apps|update-storage)
apps|add-apps|update-storage)
WOLF_CFG=/etc/wolf/cfg/config.toml
GAME_DIR="${2}"
# update-storage: read path from .env if not given on command line
if [ "$1" = "update-storage" ] && [ -z "$GAME_DIR" ]; then
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env"
if [ -f "$ENV_FILE" ]; then
GAME_DIR=$(grep '^GAME_STORAGE_DIR=' "$ENV_FILE" | cut -d= -f2-)
fi
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Resolve game storage dir: explicit arg > .env > prompt
GAME_DIR="${2:-}"
if [ -z "$GAME_DIR" ]; then
echo "Usage: ./manage.sh add-apps <path>"
echo " ./manage.sh update-storage [<new-path>]"
echo ""
echo " <path> root of game storage, e.g. /home/user/drives/games"
echo " update-storage with no path reads GAME_STORAGE_DIR from .env"
exit 1
GAME_DIR=$(grep '^GAME_STORAGE_DIR=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d= -f2-)
fi
if [ -z "$GAME_DIR" ]; then
read -r -p " Game storage path: " GAME_DIR
fi
if [ -z "$GAME_DIR" ]; then echo "No game storage path."; exit 1; fi
# Persist path back to .env if it changed
if grep -qs '^GAME_STORAGE_DIR=' "$SCRIPT_DIR/.env" 2>/dev/null; then
sed -i "s|^GAME_STORAGE_DIR=.*|GAME_STORAGE_DIR=${GAME_DIR}|" "$SCRIPT_DIR/.env"
else
echo "GAME_STORAGE_DIR=${GAME_DIR}" >> "$SCRIPT_DIR/.env"
fi
if [ ! -f "$WOLF_CFG" ]; then
echo "Wolf config not found at $WOLF_CFG — is Wolf running?"
exit 1
fi
# Persist new path in .env when update-storage is called with explicit path
if [ "$1" = "update-storage" ] && [ -n "${2:-}" ]; then
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
sed -i "s|^GAME_STORAGE_DIR=.*|GAME_STORAGE_DIR=${GAME_DIR}|" "$SCRIPT_DIR/.env" 2>/dev/null \
|| echo "GAME_STORAGE_DIR=${GAME_DIR}" >> "$SCRIPT_DIR/.env"
echo "Updated .env: GAME_STORAGE_DIR=${GAME_DIR}"
# Show which apps are already installed and what's available
echo ""
echo " Checking current Wolf config..."
INSTALLED=$(grep "^ name = 'Wolf" "$WOLF_CFG" 2>/dev/null | sed "s/.*name = '//;s/'//" | tr '\n' ' ')
[ -n "$INSTALLED" ] && echo " Already installed: $INSTALLED" || echo " No Wolf apps installed yet."
echo ""
echo " Available apps:"
echo " 1) Steam - Big Picture + Proton (PC games)"
echo " 2) EmulationStation - ES-DE + RetroArch (retro ROMs)"
echo " 3) Lutris - GOG / Epic / Wine / non-Steam"
echo " 4) RetroArch - standalone emulator frontend"
echo " 5) Prism Launcher - Minecraft (Java + Bedrock)"
echo " 6) Kodi - media center"
echo " 7) Firefox - browser"
echo " 8) Desktop - full XFCE desktop session"
echo ""
echo " Enter numbers (space-separated), 'all', or Enter to update existing mounts only:"
read -r -p " Apps [Enter=update mounts only]: " _PICKS
if [[ "${_PICKS,,}" == "all" ]]; then
APP_KEYS="steam esde lutris retroarch prismlauncher kodi firefox desktop"
elif [ -z "$_PICKS" ]; then
# No new apps — just re-run with whatever keys are already installed
APP_KEYS=""
[[ "$INSTALLED" == *WolfSteam* ]] && APP_KEYS="$APP_KEYS steam"
[[ "$INSTALLED" == *WolfES-DE* ]] && APP_KEYS="$APP_KEYS esde"
[[ "$INSTALLED" == *WolfLutris* ]] && APP_KEYS="$APP_KEYS lutris"
[[ "$INSTALLED" == *WolfRetroArch* ]] && APP_KEYS="$APP_KEYS retroarch"
[[ "$INSTALLED" == *WolfPrismLauncher* ]] && APP_KEYS="$APP_KEYS prismlauncher"
[[ "$INSTALLED" == *WolfKodi* ]] && APP_KEYS="$APP_KEYS kodi"
[[ "$INSTALLED" == *WolfFirefox* ]] && APP_KEYS="$APP_KEYS firefox"
[[ "$INSTALLED" == *WolfDesktop* ]] && APP_KEYS="$APP_KEYS desktop"
APP_KEYS="${APP_KEYS# }"
[ -z "$APP_KEYS" ] && APP_KEYS="steam esde"
else
APP_KEYS=""
for _n in $_PICKS; do
case "$_n" in
1) APP_KEYS="$APP_KEYS steam" ;;
2) APP_KEYS="$APP_KEYS esde" ;;
3) APP_KEYS="$APP_KEYS lutris" ;;
4) APP_KEYS="$APP_KEYS retroarch" ;;
5) APP_KEYS="$APP_KEYS prismlauncher" ;;
6) APP_KEYS="$APP_KEYS kodi" ;;
7) APP_KEYS="$APP_KEYS firefox" ;;
8) APP_KEYS="$APP_KEYS desktop" ;;
esac
done
APP_KEYS="${APP_KEYS# }"
fi
python3 - "$GAME_DIR" "$WOLF_CFG" << 'PYEOF'
import sys
echo " Applying: ${APP_KEYS:-none}"
[ -z "$APP_KEYS" ] && exit 0
games = sys.argv[1].rstrip('/')
cfg = sys.argv[2]
python3 - "$GAME_DIR" "$WOLF_CFG" $APP_KEYS << 'PYEOF'
import sys, json
games = sys.argv[1].rstrip('/')
cfg = sys.argv[2]
selected = set(sys.argv[3:])
STD_CAP = ['NET_RAW', 'MKNOD', 'NET_ADMIN']
STD_ENV = ['RUN_SWAY=1', 'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*']
STD_RULES = ['c 13:* rmw', 'c 244:* rmw']
CATALOG = {
'steam': dict(
name='WolfSteam', title='Steam',
icon='https://games-on-whales.github.io/wildlife/apps/steam/assets/icon.png',
image='ghcr.io/games-on-whales/steam:edge',
mounts=[f'{games}/steam:/home/retro/.steam:rw'],
env=['PROTON_LOG=1', 'RUN_SWAY=true',
'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*'],
cap_add=['SYS_ADMIN', 'SYS_NICE', 'SYS_PTRACE', 'NET_RAW', 'MKNOD', 'NET_ADMIN'],
security_opt=['seccomp=unconfined', 'apparmor=unconfined'],
ipc_mode='host', ulimits=[{'Name': 'nofile', 'Hard': 10240, 'Soft': 10240}],
privileged=False,
),
'esde': dict(
name='WolfES-DE', title='EmulationStation',
icon='https://games-on-whales.github.io/wildlife/apps/es-de/assets/icon.png',
image='ghcr.io/games-on-whales/es-de:edge',
mounts=[f'{games}/roms:/ROMs:rw',
f'{games}/saves:/home/retro/.config/retroarch/saves:rw',
f'{games}/media:/media:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'lutris': dict(
name='WolfLutris', title='Lutris',
icon='https://games-on-whales.github.io/wildlife/apps/lutris/assets/icon.png',
image='ghcr.io/games-on-whales/lutris:edge',
mounts=[f'{games}/lutris:/home/retro/.local/share/lutris:rw'],
env=['RUN_SWAY=true',
'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*'],
cap_add=['SYS_ADMIN', 'NET_RAW', 'MKNOD', 'NET_ADMIN'],
security_opt=['seccomp=unconfined', 'apparmor=unconfined'],
ipc_mode='host', ulimits=[], privileged=False,
),
'retroarch': dict(
name='WolfRetroArch', title='RetroArch',
icon='https://games-on-whales.github.io/wildlife/apps/retroarch/assets/icon.png',
image='ghcr.io/games-on-whales/retroarch:edge',
mounts=[f'{games}/roms:/ROMs:rw',
f'{games}/saves:/home/retro/.config/retroarch/saves:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'prismlauncher': dict(
name='WolfPrismLauncher', title='Prism Launcher',
icon='https://games-on-whales.github.io/wildlife/apps/prismlauncher/assets/icon.png',
image='ghcr.io/games-on-whales/prismlauncher:edge',
mounts=[f'{games}/minecraft:/home/retro/.local/share/PrismLauncher:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'kodi': dict(
name='WolfKodi', title='Kodi',
icon='https://games-on-whales.github.io/wildlife/apps/kodi/assets/icon.png',
image='ghcr.io/games-on-whales/kodi:edge',
mounts=[f'{games}/kodi:/home/retro/.kodi:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'firefox': dict(
name='WolfFirefox', title='Firefox',
icon='https://games-on-whales.github.io/wildlife/apps/firefox/assets/icon.png',
image='ghcr.io/games-on-whales/firefox:edge',
mounts=[f'{games}/firefox:/home/retro/.mozilla:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'desktop': dict(
name='WolfDesktop', title='Desktop',
icon='https://games-on-whales.github.io/wildlife/apps/desktop/assets/icon.png',
image='ghcr.io/games-on-whales/desktop:edge',
mounts=[],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
}
def make_app_block(app):
host_cfg = {'IpcMode': app['ipc_mode'], 'CapAdd': app['cap_add'],
'Privileged': app['privileged'], 'DeviceCgroupRules': STD_RULES}
if app['security_opt']: host_cfg['SecurityOpt'] = app['security_opt']
if app['ulimits']: host_cfg['Ulimits'] = app['ulimits']
create_json = json.dumps({'HostConfig': host_cfg}, indent=2)
mounts_str = str(app['mounts']).replace('"', "'")
env_str = str(app['env']).replace('"', "'")
return (
f"\n"
f" [[profiles.apps]]\n"
f" icon_png_path = '{app['icon']}'\n"
f" start_virtual_compositor = true\n"
f" title = '{app['title']}'\n"
f"\n"
f" [profiles.apps.runner]\n"
f" base_create_json = '''{create_json}\n"
f"'''\n"
f" devices = []\n"
f" env = {env_str}\n"
f" image = '{app['image']}'\n"
f" mounts = {mounts_str}\n"
f" name = '{app['name']}'\n"
f" ports = []\n"
f" type = 'docker'\n"
)
def update_mounts(lines, wolf_name, new_mounts):
for i, line in enumerate(lines):
if f"name = '{wolf_name}'" in line:
for j in range(max(0, i-10), min(len(lines), i+15)):
if lines[j].strip().startswith('mounts ='):
lines[j] = f" mounts = {new_mounts}\n"
return True
return False
with open(cfg, 'r') as f:
lines = f.readlines()
@@ -856,105 +1093,41 @@ for i, line in enumerate(lines):
if first_start is None:
print('ERROR: no [[profiles]] section found'); sys.exit(1)
def update_mounts(lines, app_name, new_mounts_line):
for i, line in enumerate(lines):
if f"name = '{app_name}'" in line:
# scan forward for the mounts line within this app block
for j in range(i - 10, i + 10):
if 0 <= j < len(lines) and lines[j].strip().startswith('mounts ='):
lines[j] = new_mounts_line
return True
return False
first_block = lines[first_start:insert_at]
has_steam = any("name = 'WolfSteam'" in l for l in first_block)
has_esde = any("name = 'WolfES-DE'" in l for l in first_block)
added, updated, to_insert = [], [], []
updated = []
if has_steam:
new_line = f" mounts = [ '{games}/steam:/home/retro/.steam:rw' ]\n"
if update_mounts(lines, 'WolfSteam', new_line):
updated.append('Steam mounts updated')
if has_esde:
new_line = f" mounts = [ '{games}/roms:/ROMs:rw', '{games}/saves:/home/retro/.config/retroarch/saves:rw', '{games}/media:/media:rw' ]\n"
if update_mounts(lines, 'WolfES-DE', new_line):
updated.append('ES-DE mounts updated')
first_block = lines[first_start:insert_at]
has_steam = any("name = 'WolfSteam'" in l for l in first_block)
has_esde = any("name = 'WolfES-DE'" in l for l in first_block)
to_insert = []
if not has_steam:
to_insert += [
'\n',
" [[profiles.apps]]\n",
" icon_png_path = 'https://games-on-whales.github.io/wildlife/apps/steam/assets/icon.png'\n",
" start_virtual_compositor = true\n",
" title = 'Steam'\n",
'\n',
" [profiles.apps.runner]\n",
" base_create_json = '''{\n",
' "HostConfig": {\n',
' "IpcMode": "host",\n',
' "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"],\n',
' "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"],\n',
' "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}],\n',
' "Privileged": false,\n',
' "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"]\n',
' }\n',
"}\n",
"'''\n",
" devices = []\n",
" env = [ 'PROTON_LOG=1', 'RUN_SWAY=true', 'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*' ]\n",
" image = 'ghcr.io/games-on-whales/steam:edge'\n",
" mounts = [ '" + games + "/steam:/home/retro/.steam:rw' ]\n",
" name = 'WolfSteam'\n",
" ports = []\n",
" type = 'docker'\n",
]
if not has_esde:
to_insert += [
'\n',
" [[profiles.apps]]\n",
" icon_png_path = 'https://games-on-whales.github.io/wildlife/apps/es-de/assets/icon.png'\n",
" start_virtual_compositor = true\n",
" title = 'EmulationStation'\n",
'\n',
" [profiles.apps.runner]\n",
" base_create_json = '''{\n",
' "HostConfig": {\n',
' "IpcMode": "host",\n',
' "Privileged": false,\n',
' "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN"],\n',
' "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"]\n',
' }\n',
"}\n",
"'''\n",
" devices = []\n",
" env = [ 'RUN_SWAY=1', 'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*' ]\n",
" image = 'ghcr.io/games-on-whales/es-de:edge'\n",
" mounts = [ '" + games + "/roms:/ROMs:rw', '" + games + "/saves:/home/retro/.config/retroarch/saves:rw', '" + games + "/media:/media:rw' ]\n",
" name = 'WolfES-DE'\n",
" ports = []\n",
" type = 'docker'\n",
]
for key in list(CATALOG.keys()):
if key not in selected: continue
app = CATALOG[key]
wolf_name = app['name']
new_mounts = str(app['mounts']).replace('"', "'")
already = any(f"name = '{wolf_name}'" in l for l in first_block)
if already:
if update_mounts(lines, wolf_name, new_mounts):
updated.append(app['title'])
else:
to_insert.append(make_app_block(app))
added.append(app['title'])
if to_insert:
new_lines = lines[:insert_at] + to_insert + lines[insert_at:]
block_lines = []
for block in to_insert:
block_lines += [l + '\n' if not l.endswith('\n') else l
for l in block.splitlines()]
new_lines = lines[:insert_at] + block_lines + lines[insert_at:]
with open(cfg, 'w') as f:
f.writelines(new_lines)
added = [n for n, exists in [('Steam', has_steam), ('EmulationStation', has_esde)] if not exists]
print(f"Added to default profile: {', '.join(added)}")
elif updated:
with open(cfg, 'w') as f:
f.writelines(lines)
print('; '.join(updated))
else:
print('Both apps already in profile (paths unchanged)')
if added: print(f"Added: {', '.join(added)}")
if updated: print(f"Updated mounts: {', '.join(updated)}")
if not added and not updated:
print('All selected apps already present and up to date')
PYEOF
docker compose restart wolf
echo "Wolf restarted. Steam and EmulationStation should now appear in Moonlight."
echo "Wolf restarted. Apps will appear in Moonlight on next connection."
;;
backup)
echo "Set up backups with the modular system: sudo ./setup.sh backup"
@@ -997,9 +1170,7 @@ PYEOF
echo " ./manage.sh status - Show Wolf + app containers"
echo " ./manage.sh pin - Show recent Moonlight pairing PIN link"
echo " ./manage.sh update - Pull latest Wolf image and restart"
echo " ./manage.sh add-apps <path> - Add Steam + ES-DE to Wolf config"
echo " ./manage.sh update-storage [<new-path>] - Update game storage path in Wolf config"
echo " (reads .env if no path given)"
echo " ./manage.sh apps - Add / update game launchers in Wolf"
echo " ./manage.sh backup - How to set up backups"
;;
esac
@@ -1021,19 +1192,139 @@ MEOF
done
if [ -f "$WOLF_CFG" ]; then
log_info "Adding Steam and EmulationStation to Wolf config..."
python3 - "$GAME_STORAGE_DIR" "$WOLF_CFG" << 'PYEOF'
import sys, re
log_info "Injecting selected apps into Wolf config..."
python3 - "$GAME_STORAGE_DIR" "$WOLF_CFG" $([[ -n "$_APP_KEYS" ]] && echo "$_APP_KEYS" || echo "steam esde") << 'PYEOF'
import sys, json
games = sys.argv[1].rstrip('/')
cfg = sys.argv[2]
games = sys.argv[1].rstrip('/')
cfg = sys.argv[2]
selected = set(sys.argv[3:]) # app keys chosen by the user
# ── App catalog ───────────────────────────────────────────────────────────────
# Each entry: (wolf_name, title, icon_url, image, mounts, env, cap_add,
# security_opt, ipc_mode, ulimits, privileged)
STD_CAP = ['NET_RAW', 'MKNOD', 'NET_ADMIN']
STD_ENV = ['RUN_SWAY=1', 'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*']
STD_RULES = ['c 13:* rmw', 'c 244:* rmw']
CATALOG = {
'steam': dict(
name='WolfSteam', title='Steam',
icon='https://games-on-whales.github.io/wildlife/apps/steam/assets/icon.png',
image='ghcr.io/games-on-whales/steam:edge',
mounts=[f'{games}/steam:/home/retro/.steam:rw'],
env=['PROTON_LOG=1', 'RUN_SWAY=true',
'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*'],
cap_add=['SYS_ADMIN', 'SYS_NICE', 'SYS_PTRACE', 'NET_RAW', 'MKNOD', 'NET_ADMIN'],
security_opt=['seccomp=unconfined', 'apparmor=unconfined'],
ipc_mode='host',
ulimits=[{'Name': 'nofile', 'Hard': 10240, 'Soft': 10240}],
privileged=False,
),
'esde': dict(
name='WolfES-DE', title='EmulationStation',
icon='https://games-on-whales.github.io/wildlife/apps/es-de/assets/icon.png',
image='ghcr.io/games-on-whales/es-de:edge',
mounts=[f'{games}/roms:/ROMs:rw',
f'{games}/saves:/home/retro/.config/retroarch/saves:rw',
f'{games}/media:/media:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'lutris': dict(
name='WolfLutris', title='Lutris',
icon='https://games-on-whales.github.io/wildlife/apps/lutris/assets/icon.png',
image='ghcr.io/games-on-whales/lutris:edge',
mounts=[f'{games}/lutris:/home/retro/.local/share/lutris:rw'],
env=['RUN_SWAY=true',
'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*'],
cap_add=['SYS_ADMIN', 'NET_RAW', 'MKNOD', 'NET_ADMIN'],
security_opt=['seccomp=unconfined', 'apparmor=unconfined'],
ipc_mode='host', ulimits=[], privileged=False,
),
'retroarch': dict(
name='WolfRetroArch', title='RetroArch',
icon='https://games-on-whales.github.io/wildlife/apps/retroarch/assets/icon.png',
image='ghcr.io/games-on-whales/retroarch:edge',
mounts=[f'{games}/roms:/ROMs:rw',
f'{games}/saves:/home/retro/.config/retroarch/saves:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'prismlauncher': dict(
name='WolfPrismLauncher', title='Prism Launcher',
icon='https://games-on-whales.github.io/wildlife/apps/prismlauncher/assets/icon.png',
image='ghcr.io/games-on-whales/prismlauncher:edge',
mounts=[f'{games}/minecraft:/home/retro/.local/share/PrismLauncher:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'kodi': dict(
name='WolfKodi', title='Kodi',
icon='https://games-on-whales.github.io/wildlife/apps/kodi/assets/icon.png',
image='ghcr.io/games-on-whales/kodi:edge',
mounts=[f'{games}/kodi:/home/retro/.kodi:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'firefox': dict(
name='WolfFirefox', title='Firefox',
icon='https://games-on-whales.github.io/wildlife/apps/firefox/assets/icon.png',
image='ghcr.io/games-on-whales/firefox:edge',
mounts=[f'{games}/firefox:/home/retro/.mozilla:rw'],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
'desktop': dict(
name='WolfDesktop', title='Desktop',
icon='https://games-on-whales.github.io/wildlife/apps/desktop/assets/icon.png',
image='ghcr.io/games-on-whales/desktop:edge',
mounts=[],
env=STD_ENV, cap_add=STD_CAP, security_opt=[], ipc_mode='host',
ulimits=[], privileged=False,
),
}
def make_app_block(app):
"""Render a [[profiles.apps]] TOML block from a catalog entry."""
host_cfg = {'IpcMode': app['ipc_mode'], 'CapAdd': app['cap_add'],
'Privileged': app['privileged'], 'DeviceCgroupRules': STD_RULES}
if app['security_opt']: host_cfg['SecurityOpt'] = app['security_opt']
if app['ulimits']: host_cfg['Ulimits'] = app['ulimits']
create_json = json.dumps({'HostConfig': host_cfg}, indent=2)
mounts_str = str(app['mounts']).replace('"', "'")
env_str = str(app['env']).replace('"', "'")
return (
f"\n"
f" [[profiles.apps]]\n"
f" icon_png_path = '{app['icon']}'\n"
f" start_virtual_compositor = true\n"
f" title = '{app['title']}'\n"
f"\n"
f" [profiles.apps.runner]\n"
f" base_create_json = '''{create_json}\n"
f"'''\n"
f" devices = []\n"
f" env = {env_str}\n"
f" image = '{app['image']}'\n"
f" mounts = {mounts_str}\n"
f" name = '{app['name']}'\n"
f" ports = []\n"
f" type = 'docker'\n"
)
def update_mounts(lines, wolf_name, new_mounts):
for i, line in enumerate(lines):
if f"name = '{wolf_name}'" in line:
for j in range(max(0, i-10), min(len(lines), i+15)):
if lines[j].strip().startswith('mounts ='):
lines[j] = f" mounts = {new_mounts}\n"
return True
return False
with open(cfg, 'r') as f:
lines = f.readlines()
# Wolf uses [[profiles]] / [[profiles.apps]] format.
# Apps must be inserted into the FIRST profile (the default paired-client
# profile) before the second [[profiles]] section starts.
profiles_seen, first_start, insert_at = 0, None, len(lines)
for i, line in enumerate(lines):
if line.strip() == '[[profiles]]':
@@ -1045,109 +1336,46 @@ if first_start is None:
print('[ERROR] No [[profiles]] section found in config'); sys.exit(1)
first_block = lines[first_start:insert_at]
has_steam = any("name = 'WolfSteam'" in l for l in first_block)
has_esde = any("name = 'WolfES-DE'" in l for l in first_block)
# If an app already exists, update its mounts line in place rather than skip.
def update_mounts(lines, app_name, new_mounts_line):
in_app = False
for i, line in enumerate(lines):
if f"name = '{app_name}'" in line:
in_app = True
if in_app and line.strip().startswith('mounts ='):
lines[i] = new_mounts_line
return True
return False
updated = []
if has_steam:
new_line = f" mounts = [ '{games}/steam:/home/retro/.steam:rw' ]\n"
if update_mounts(lines, 'WolfSteam', new_line):
updated.append('Steam mounts updated')
if has_esde:
new_line = f" mounts = [ '{games}/roms:/ROMs:rw', '{games}/saves:/home/retro/.config/retroarch/saves:rw', '{games}/media:/media:rw' ]\n"
if update_mounts(lines, 'WolfES-DE', new_line):
updated.append('ES-DE mounts updated')
# Re-derive first_block after in-place updates
first_block = lines[first_start:insert_at]
has_steam = any("name = 'WolfSteam'" in l for l in first_block)
has_esde = any("name = 'WolfES-DE'" in l for l in first_block)
added, updated = [], []
to_insert = []
if not has_steam:
to_insert += [
'\n',
" [[profiles.apps]]\n",
" icon_png_path = 'https://games-on-whales.github.io/wildlife/apps/steam/assets/icon.png'\n",
" start_virtual_compositor = true\n",
" title = 'Steam'\n",
'\n',
" [profiles.apps.runner]\n",
" base_create_json = '''{\n",
' "HostConfig": {\n',
' "IpcMode": "host",\n',
' "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"],\n',
' "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"],\n',
' "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}],\n',
' "Privileged": false,\n',
' "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"]\n',
' }\n',
"}\n",
"'''\n",
" devices = []\n",
" env = [ 'PROTON_LOG=1', 'RUN_SWAY=true', 'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*' ]\n",
" image = 'ghcr.io/games-on-whales/steam:edge'\n",
f" mounts = [ '{games}/steam:/home/retro/.steam:rw' ]\n",
" name = 'WolfSteam'\n",
" ports = []\n",
" type = 'docker'\n",
]
if not has_esde:
to_insert += [
'\n',
" [[profiles.apps]]\n",
" icon_png_path = 'https://games-on-whales.github.io/wildlife/apps/es-de/assets/icon.png'\n",
" start_virtual_compositor = true\n",
" title = 'EmulationStation'\n",
'\n',
" [profiles.apps.runner]\n",
" base_create_json = '''{\n",
' "HostConfig": {\n',
' "IpcMode": "host",\n',
' "Privileged": false,\n',
' "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN"],\n',
' "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"]\n',
' }\n',
"}\n",
"'''\n",
" devices = []\n",
" env = [ 'RUN_SWAY=1', 'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*' ]\n",
" image = 'ghcr.io/games-on-whales/es-de:edge'\n",
f" mounts = [ '{games}/roms:/ROMs:rw', '{games}/saves:/home/retro/.config/retroarch/saves:rw', '{games}/media:/media:rw' ]\n",
" name = 'WolfES-DE'\n",
" ports = []\n",
" type = 'docker'\n",
]
for key in list(CATALOG.keys()): # preserve display order
if key not in selected:
continue
app = CATALOG[key]
wolf_name = app['name']
already = any(f"name = '{wolf_name}'" in l for l in first_block)
new_mounts = str(app['mounts']).replace('"', "'")
if already:
if update_mounts(lines, wolf_name, new_mounts):
updated.append(app['title'])
else:
to_insert.append(make_app_block(app))
added.append(app['title'])
if to_insert:
new_lines = lines[:insert_at] + to_insert + lines[insert_at:]
block_lines = []
for block in to_insert:
block_lines += [l + '\n' if not l.endswith('\n') else l
for l in block.splitlines()]
new_lines = lines[:insert_at] + block_lines + lines[insert_at:]
with open(cfg, 'w') as f:
f.writelines(new_lines)
added = [n for n, exists in [('Steam', has_steam), ('EmulationStation', has_esde)] if not exists]
print(f"[INFO] Added to default profile: {', '.join(added)}")
elif updated:
with open(cfg, 'w') as f:
f.writelines(lines)
print(f"[INFO] {'; '.join(updated)}")
else:
print('[INFO] Steam and EmulationStation already in default profile (paths unchanged)')
if added: print(f"[INFO] Added: {', '.join(added)}")
if updated: print(f"[INFO] Updated mounts: {', '.join(updated)}")
if not added and not updated:
print('[INFO] All selected apps already present and up to date')
PYEOF
docker compose restart wolf
log_success "Wolf restarted with updated config"
else
log_warning "Wolf config not generated in time. Add apps manually to /etc/wolf/cfg/config.toml"
log_warning "Then run: ./manage.sh restart"
log_warning "Then run: ./manage.sh apps"
fi
# Hand the folder back to the real user
@@ -1238,7 +1466,7 @@ PYEOF
echo " Online together → each player launches their own session,"
echo " all connect to the same game server"
echo ""
echo "Manage: cd $WOLF_DIR && ./manage.sh {start|stop|restart|logs|status|pin|update|add-apps}"
echo "Manage: cd $WOLF_DIR && ./manage.sh {start|stop|restart|logs|status|pin|update|apps}"
echo ""
echo "── BACKUPS ───────────────────────────────────────────"
echo ""
@@ -1284,7 +1512,7 @@ cd $WOLF_DIR
./manage.sh logs # live logs
./manage.sh status # container status
./manage.sh update # pull latest image and restart
./manage.sh add-apps # add game launchers
./manage.sh apps # add / update game launchers
\`\`\`
## Ports (open on firewall / router)