Add desktop shortcuts for installed games.
This commit is contained in:
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
|
||||||
|
|||||||
@@ -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}."
|
||||||
|
}
|
||||||
@@ -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
@@ -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 ;;
|
||||||
|
|||||||
Reference in new issue
Block a user