Add desktop shortcuts for installed games.

This commit is contained in:
Storm Dragon committed 2026-11-05 15:53:23 -05:00
1 parent fbafaed846
commit 40803a7162
4 files changed
+152 -19

No files matched your search

@@ -1,6 +1,6 @@
# Linux Game Manager Core Map # Linux Game Manager Core Map
Last refreshed: 2026-10-31 Last refreshed: 2026-11-05
## Top-Level Structure ## Top-Level Structure
@@ -9,6 +9,7 @@ Last refreshed: 2026-10-31
- `.launch/`: Per-game launch scripts (`.game`) and runnable entries (`.sh`, usually symlinks). - `.launch/`: Per-game launch scripts (`.game`) and runnable entries (`.sh`, usually symlinks).
- `.update/`: Optional per-game update scripts expected to define `run_update()`. - `.update/`: Optional per-game update scripts expected to define `run_update()`.
- `.includes/ipfs.sh`: Central associative array of IPFS download URLs, expanded against the configured gateway at startup. - `.includes/ipfs.sh`: Central associative array of IPFS download URLs, expanded against the configured gateway at startup.
- `.includes/desktop.sh`: Game-specific desktop shortcuts with Desktop Entry and Exec argument escaping.
- `.includes/wickets.sh`: Shared Wickets installer/updater; resolves the latest GitHub release and preserves portable saves. - `.includes/wickets.sh`: Shared Wickets installer/updater; resolves the latest GitHub release and preserves portable saves.
- `speech/speak_window_title.sh`: Accessibility helper for announcing focused window titles. - `speech/speak_window_title.sh`: Accessibility helper for announcing focused window titles.
- `README.md`: Project summary and high-level behavior notes. - `README.md`: Project summary and high-level behavior notes.
@@ -43,6 +44,10 @@ python3 .codex/skills/linux-game-manager-dev/scripts/audit_game_catalog.py
- `game_launcher` enumerates `.launch/*.sh` entries. - `game_launcher` enumerates `.launch/*.sh` entries.
- Entries with first line `#//` are skipped. - Entries with first line `#//` are skipped.
- Selected launcher is sourced/executed. - Selected launcher is sourced/executed.
- The launch menu offers Desktop Shortcut in both Yad and dialog. It writes an executable `~/Desktop/linux-game-manager-<Game Name>.desktop`, refuses to overwrite existing files, and returns to the menu. The shortcut runs the manager with `-l` and sets its working directory to the repository root.
- `game_launcher <Game Name>` skips the menu and selects an installed, enabled entry by its exact display name. Missing or disabled entries fail with a message. Launcher discovery uses `scriptRoot` even when called from another working directory.
- `ui_menu --desktop-shortcut` adds the launch-menu buttons and returns status 3 for shortcut creation. Ordinary `ui_menu` calls keep their standard buttons. Both modes preserve cancellation status and discard any cancelled selection.
- Run `bash tests/game_shortcut_tests.sh` for isolated menu, direct-launch, CLI, and Desktop Entry checks; these require Python with PyGObject and `desktop-file-validate`. They do not launch installed games or use real user state. Live keyboard, screen-reader, and real-game acceptance remains separate.
- Monkey Spank runs from its repository directory so relative sound and text assets resolve; its explicit directory assignment also identifies the removal root. - Monkey Spank runs from its repository directory so relative sound and text assets resolve; its explicit directory assignment also identifies the removal root.
- Text-terminal launchers can fall back to `xterm`, which inherits the requested working directory before executing the game command. - Text-terminal launchers can fall back to `xterm`, which inherits the requested working directory before executing the game command.
@@ -60,6 +65,7 @@ python3 .codex/skills/linux-game-manager-dev/scripts/audit_game_catalog.py
## CLI Surface ## CLI Surface
- `-i`: install game - `-i`: install game
- `-l <Game Name>`: launch an installed, enabled game directly by exact menu name
- `-r`: remove game - `-r`: remove game
- `-u`: update game - `-u`: update game
- `-t`: show number of available games - `-t`: show number of available games
+54
View File
@@ -0,0 +1,54 @@
# shellcheck shell=bash
# Desktop Entry escaping is separate from Exec argument quoting.
desktop_entry_value() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//$'\n'/\\n}"
value="${value//$'\r'/\\r}"
value="${value//$'\t'/\\t}"
printf '%s' "$value"
}
desktop_exec_argument() {
local value="$1"
value="${value//%/%%}"
value="${value//\\/\\\\}"
value="${value//\"/\\\"}"
value="${value//\$/\\\$}"
value="${value//\`/\\\`}"
desktop_entry_value "\"${value}\""
}
game_desktop_shortcut() {
local gameName="$1"
local fileName="${gameName//\//_}"
fileName="${fileName//[$'\n\r\t']/_}"
local desktopFile="${HOME}/Desktop/linux-game-manager-${fileName}.desktop"
local launcherPath
# shellcheck disable=SC2154 # Set by the manager before sourcing this helper.
launcherPath="$(readlink -e "${scriptRoot}/linux-game-manager.sh")" || return 1
if ! mkdir -p "${HOME}/Desktop"; then
ui_msgbox "Desktop Shortcut" "" "Could not create ~/Desktop."
return 1
fi
# Refuse to overwrite files or follow existing symbolic links.
if ! (
set -o noclobber
printf '%s\n' '[Desktop Entry]' \
"Name=$(desktop_entry_value "$gameName")" \
"Exec=$(desktop_exec_argument "$launcherPath") -l $(desktop_exec_argument "$gameName")" \
"Path=$(desktop_entry_value "$scriptRoot")" \
'Terminal=true' 'Type=Application' 'Categories=Game;' \
> "$desktopFile"
); then
ui_msgbox "Desktop Shortcut" "" "Could not create ${desktopFile}. Check permissions and whether a shortcut already exists."
return 1
fi
if ! chmod +x "$desktopFile"; then
ui_msgbox "Desktop Shortcut" "" "Could not make ${desktopFile} executable."
return 1
fi
ui_msgbox "Desktop Shortcut" "" "Created ${desktopFile}."
}
+17
View File
@@ -9,6 +9,23 @@ As the project grows, more dependencies will most likely be added. When LGM is l
Current base dependencies checked on launch: `7z`, `curl`, `dialog`, `jq`, `yad`, and `unzip`. Current base dependencies checked on launch: `7z`, `curl`, `dialog`, `jq`, `yad`, and `unzip`.
## Desktop shortcuts
Open `./linux-game-manager.sh`, select an installed game, and choose **Desktop
Shortcut**. This works in both the graphical and console menus and creates an
executable `.desktop` file in `~/Desktop`. Existing files are left intact, and
the game list opens again after creation. Your desktop environment may ask you
to trust the shortcut before launching it.
The shortcut launches that game directly through the manager's `-l` option.
You can also use this option from a terminal with the exact game menu name:
```bash
./linux-game-manager.sh -l "RS Games"
```
Only installed, enabled launcher entries can be launched this way.
## Wickets ## Wickets
Install Wickets on x86_64 Linux by selecting it under `./linux-game-manager.sh -i`. Install Wickets on x86_64 Linux by selecting it under `./linux-game-manager.sh -i`.
+74 -18
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# shellcheck disable=SC2329 # Shared helpers are called by sourced game scripts.
scriptRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" scriptRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
@@ -50,14 +51,25 @@ export DIALOGOPTS='--no-lines --visit-items'
# Automatically switches between dialog (console) and yad (GUI) based on DISPLAY environment # Automatically switches between dialog (console) and yad (GUI) based on DISPLAY environment
# Wrapper function for menu selection # Wrapper function for menu selection
# Usage: ui_menu "title" "backtitle" "text" option1 "description1" option2 "description2" ... # Usage: ui_menu [--desktop-shortcut] "title" "backtitle" "text" option "description" ...
# Returns 3 for Desktop Shortcut; other nonzero statuses cancel the menu.
ui_menu() { ui_menu() {
local shortcutMenu=false
local menuButtons=()
local menuCode=0
if [[ "$1" == --desktop-shortcut ]]; then
shortcutMenu=true
shift
fi
local title="$1" local title="$1"
local back_title="$2" local back_title="$2"
local text="$3" local text="$3"
shift 3 shift 3
if [[ "$dialog_type" == "yad" ]]; then if [[ "$dialog_type" == "yad" ]]; then
if [[ "$shortcutMenu" == true ]]; then
menuButtons=(--button="Launch:0" --button="Desktop Shortcut:2" --button="Cancel:1")
fi
# Build yad list format: Display only, then map back to value # Build yad list format: Display only, then map back to value
local yad_list="" local yad_list=""
declare -A value_map declare -A value_map
@@ -81,7 +93,13 @@ ui_menu() {
--selectable-labels \ --selectable-labels \
--search-column=1 \ --search-column=1 \
--height=400 \ --height=400 \
--width=600) --width=600 "${menuButtons[@]}") || menuCode=$?
if [[ "$shortcutMenu" == true && $menuCode -eq 2 ]]; then
menuCode=3
fi
if [[ $menuCode -ne 0 && ( "$shortcutMenu" != true || $menuCode -ne 3 ) ]]; then
return "$menuCode"
fi
# Strip trailing newline, pipe, and any other whitespace # Strip trailing newline, pipe, and any other whitespace
selected_description=$(echo "$selected_description" | tr -d '\n\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/|$//') selected_description=$(echo "$selected_description" | tr -d '\n\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/|$//')
@@ -89,7 +107,11 @@ ui_menu() {
if [[ -n "$selected_description" ]]; then if [[ -n "$selected_description" ]]; then
echo "${value_map["$selected_description"]}" echo "${value_map["$selected_description"]}"
fi fi
return "$menuCode"
else else
if [[ "$shortcutMenu" == true ]]; then
menuButtons=(--extra-button --extra-label "Desktop Shortcut")
fi
# Build dialog menu format with mapping (same approach as yad) # Build dialog menu format with mapping (same approach as yad)
local dialog_args=() local dialog_args=()
declare -A value_map declare -A value_map
@@ -104,15 +126,20 @@ ui_menu() {
local selected_description local selected_description
selected_description=$(dialog --backtitle "$back_title" \ selected_description=$(dialog --backtitle "$back_title" \
--title "$title" \ --title "$title" \
"${menuButtons[@]}" \
--no-tags \ --no-tags \
--menu "$text" 0 0 0 \ --menu "$text" 0 0 0 \
"${dialog_args[@]}" \ "${dialog_args[@]}" \
--stdout) --stdout) || menuCode=$?
if [[ $menuCode -ne 0 && ( "$shortcutMenu" != true || $menuCode -ne 3 ) ]]; then
return "$menuCode"
fi
# Return the mapped value # Return the mapped value
if [[ -n "$selected_description" ]]; then if [[ -n "$selected_description" ]]; then
echo "${value_map["$selected_description"]}" echo "${value_map["$selected_description"]}"
fi fi
return "$menuCode"
fi fi
} }
@@ -878,7 +905,7 @@ game_update() {
game_launcher() { game_launcher() {
# Initialize array for menu construction # Initialize array for menu construction
mapfile -t menuList < <( mapfile -t menuList < <(
if [[ -d ".launch" ]]; then if [[ -d "${scriptRoot}/.launch" ]]; then
# shellcheck disable=SC2016 # variables expand in the child bash, not this single-quoted string # shellcheck disable=SC2016 # variables expand in the child bash, not this single-quoted string
find -L "${scriptRoot}/.launch" -maxdepth 1 -type f -iname "*.sh" -print0 | sort -z | xargs -0 bash -c ' find -L "${scriptRoot}/.launch" -maxdepth 1 -type f -iname "*.sh" -print0 | sort -z | xargs -0 bash -c '
for f; do for f; do
@@ -889,20 +916,45 @@ game_launcher() {
done' _ done' _
fi fi
) )
if [[ ${#menuList} -eq 0 ]]; then local selectedGame=""
ui_msgbox "Linux Game Manager" "Linux Game Manager" "No games found." if [[ $# -gt 0 ]]; then
exit 0 for ((i=0; i<${#menuList[@]}; i+=2)); do
fi if [[ "$1" == "${menuList[i+1]}" ]]; then
# Create the menu of all games selectedGame="${menuList[i]}"
# menuList has alternating full_path, display_name pairs break
declare -a menuArgs fi
for ((i=0; i<${#menuList[@]}; i+=2)); do done
menuArgs+=("${menuList[i]}" "${menuList[i+1]}") if [[ -z "$selectedGame" ]]; then
done ui_msgbox "Linux Game Launcher" "Linux Game Manager" "Game $1 is not installed or is disabled."
selectedGame="$(ui_menu "Linux Game Launcher" "Linux Game Launcher" "Please select a game to play" "${menuArgs[@]}")" exit 1
local menuCode=$? fi
if [[ $menuCode -ne 0 ]] || [[ -z "$selectedGame" ]]; then else
exit 0 if [[ ${#menuList[@]} -eq 0 ]]; then
ui_msgbox "Linux Game Manager" "Linux Game Manager" "No games found."
exit 0
fi
# Create the menu of all games
# menuList has alternating full_path, display_name pairs
declare -a menuArgs
for ((i=0; i<${#menuList[@]}; i+=2)); do
menuArgs+=("${menuList[i]}" "${menuList[i+1]}")
done
local menuCode
local gameName
while true; do
selectedGame="$(ui_menu --desktop-shortcut "Linux Game Launcher" "Linux Game Launcher" "Please select a game to play" "${menuArgs[@]}")"
menuCode=$?
if [[ $menuCode -eq 3 ]]; then
if [[ -n "$selectedGame" ]]; then
gameName="${selectedGame##*/}"
game_desktop_shortcut "${gameName%.sh}"
fi
continue
elif [[ $menuCode -ne 0 || -z "$selectedGame" ]]; then
exit 0
fi
break
done
fi fi
# shellcheck disable=SC1090 # launcher scripts are selected dynamically from .launch # shellcheck disable=SC1090 # launcher scripts are selected dynamically from .launch
. "${selectedGame}" . "${selectedGame}"
@@ -969,6 +1021,8 @@ export ipfsGateway="${ipfsGateway:-https://ipfs.stormux.org}"
source "${scriptRoot}/.includes/ipfs.sh" source "${scriptRoot}/.includes/ipfs.sh"
# shellcheck source=.includes/gamemode.sh # shellcheck source=.includes/gamemode.sh
source "${scriptRoot}/.includes/gamemode.sh" source "${scriptRoot}/.includes/gamemode.sh"
# shellcheck source=.includes/desktop.sh
source "${scriptRoot}/.includes/desktop.sh"
export spd_module="${spd_module:+ -o ${spd_module}}" export spd_module="${spd_module:+ -o ${spd_module}}"
export spd_pitch="${spd_pitch:+ -p ${spd_pitch}}" export spd_pitch="${spd_pitch:+ -p ${spd_pitch}}"
export spd_rate="${spd_rate:+ -r ${spd_rate}}" export spd_rate="${spd_rate:+ -r ${spd_rate}}"
@@ -1006,6 +1060,7 @@ declare -A command=(
[h]="This help screen." [h]="This help screen."
[i]="Install games." [i]="Install games."
[L]="Display license information." [L]="Display license information."
[l:]="Launch an installed game directly by its menu name."
[N]="No cache, delete the installer after it has been extracted." [N]="No cache, delete the installer after it has been extracted."
[R]="Redownload. Removes old versions of packages from cache before installing." [R]="Redownload. Removes old versions of packages from cache before installing."
[r]="Remove game. Remove a game and its menu entry." [r]="Remove game. Remove a game and its menu entry."
@@ -1023,6 +1078,7 @@ while getopts "${args}" i ; do
h) help ;; h) help ;;
i) game_installer ;; i) game_installer ;;
L) license ;; L) license ;;
l) game_launcher "$OPTARG" ;;
N) noCache="true" ;; N) noCache="true" ;;
R) redownload="true" ;; R) redownload="true" ;;
r) game_removal ;; r) game_removal ;;